diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ca857b4ee..909a3b5b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug report about: Create a report to help us improve --- -- [ ] This is an actually a bug report. +- [ ] This is actually a bug report. - [ ] I am not getting good LLM Results - [ ] I have tried asking for help in the community on discord or discussions and have not received a response. - [ ] I have tried searching the documentation and have not found an answer. diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index d054d1b6e..bc83ff0f8 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -3,11 +3,11 @@ name: Weekly Tests on: workflow_dispatch: schedule: - - cron: '0 0 * * 0' # Runs at 00:00 UTC every Sunday + - cron: "0 0 * * 0" # Runs at 00:00 UTC every Sunday push: - branches: [ main ] + branches: [main] paths-ignore: - - '**' # Ignore all paths to ensure it only triggers on schedule + - "**" # Ignore all paths to ensure it only triggers on schedule jobs: weekly-tests: @@ -20,15 +20,15 @@ jobs: uses: actions/setup-python@v4 with: python-version: 3.11 - cache: 'poetry' + cache: "poetry" - name: Install Poetry uses: snok/install-poetry@v1.3.1 - name: Install dependencies - run: poetry install --with dev + run: poetry install --with dev,anthropic - name: Run all tests run: poetry run pytest tests/ env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} \ No newline at end of file + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml deleted file mode 100644 index d1a41e50d..000000000 --- a/.github/workflows/mkdocs.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Deploy MkDocs - -on: - push: - branches: - - main - -permissions: - contents: write - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Install Poetry - uses: snok/install-poetry@v1.3.1 - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - cache: 'poetry' - - - name: Install APT packages - run: | - sudo apt-get update && - sudo apt-get install pngquant - - - name: Install via Poetry - run: poetry install --with dev,docs - - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Build and deploy MkDocs - run: poetry run mkdocs gh-deploy --force \ No newline at end of file diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml deleted file mode 100644 index d76a711d8..000000000 --- a/.github/workflows/mypy.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: MyPy - -on: - push: - pull_request: - branches: [ main ] - -env: - WORKING_DIRECTORY: "." - MYPY_OUTPUT_FILENAME: "mypy.log" - CUSTOM_FLAGS: "--python-version=3.9 --color-output --no-pretty --follow-imports=skip" - CUSTOM_PACKAGES: | - instructor/_types/_alias.py - instructor/cli/cli.py - instructor/cli/files.py - instructor/cli/jobs.py - instructor/cli/usage.py - instructor/exceptions.py - instructor/distil.py - instructor/dsl/citation.py - instructor/dsl/iterable.py - instructor/dsl/maybe.py - instructor/dsl/parallel.py - instructor/dsl/partial.py - instructor/dsl/partialjson.py - instructor/dsl/validators.py - instructor/function_calls.py - tests/test_function_calls.py - tests/test_distil.py - -jobs: - MyPy: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Environment setup - uses: actions/setup-python@v4 - with: - python-version: 3.9 - cache: "pip" - - name: Install dev dependencies - run: | - python3 -m pip install --upgrade pip setuptools wheel - python3 -m pip install -r requirements.txt - python3 -m pip install -r requirements-doc.txt - - name: Run Continuous Integration Action - run: | - set -e -o pipefail - export CUSTOM_PACKAGES="${{ env.CUSTOM_PACKAGES }}" && - export CUSTOM_FLAGS="${{ env.CUSTOM_FLAGS }}" && - curl -sSL https://raw.githubusercontent.com/gao-hongnan/omniverse/2fd5de1b8103e955cd5f022ab016b72fa901fa8f/scripts/devops/continuous-integration/type_mypy.sh -o type_mypy.sh - chmod +x type_mypy.sh - bash type_mypy.sh | tee ${{ env.WORKING_DIRECTORY }}/${{ env.MYPY_OUTPUT_FILENAME }} - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: mypy-log - path: ${{ env.WORKING_DIRECTORY }}/${{ env.MYPY_OUTPUT_FILENAME }} \ No newline at end of file diff --git a/.github/workflows/pyright.yml b/.github/workflows/pyright.yml new file mode 100644 index 000000000..12b7d67ab --- /dev/null +++ b/.github/workflows/pyright.yml @@ -0,0 +1,52 @@ +name: Pyright + +on: + push: + pull_request: + branches: [ main ] + +env: + WORKING_DIRECTORY: "." + PYRIGHT_OUTPUT_FILENAME: "pyright.log" + +jobs: + Pyright: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11"] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v2 + with: + path: ~/.cache/pypoetry/virtualenvs + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install Poetry + uses: snok/install-poetry@v1.3.1 + + - name: Install dependencies + run: poetry install --with dev,anthropic + + - name: Run Static Type Checking with Pyright + run: | + set -e -o pipefail + poetry run pyright > ${{ env.WORKING_DIRECTORY }}/${{ env.PYRIGHT_OUTPUT_FILENAME }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: pyright-log + path: ${{ env.WORKING_DIRECTORY }}/${{ env.PYRIGHT_OUTPUT_FILENAME }} diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 2c35eeac9..eaecfbe65 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -3,7 +3,7 @@ name: Ruff on: push: pull_request: - branches: [ main ] + branches: [main] env: WORKING_DIRECTORY: "." @@ -42,4 +42,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: ruff-log - path: ${{ env.WORKING_DIRECTORY }}/${{ env.RUFF_OUTPUT_FILENAME }} \ No newline at end of file + path: ${{ env.WORKING_DIRECTORY }}/${{ env.RUFF_OUTPUT_FILENAME }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c211cada3..ac9383606 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: pull_request: push: branches: - - main + - main jobs: release: @@ -11,11 +11,11 @@ jobs: strategy: matrix: - python-version: ['3.10', '3.11'] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - + - name: Set up Python uses: actions/setup-python@v4 with: @@ -33,24 +33,21 @@ jobs: uses: snok/install-poetry@v1.3.1 - name: Install dependencies - run: poetry install --with dev + run: poetry install --with dev,anthropic - name: Run tests - run: poetry run pytest tests/ -k "not openai" + if: matrix.python-version != '3.11' + run: poetry run pytest tests/ -k "not openai and not anthropic and not evals and not docs" env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Generate coverage report if: matrix.python-version == '3.11' run: | - poetry run coverage run -m pytest tests/ -k "not openai" + poetry run coverage run -m pytest tests/ -k "not docs" poetry run coverage report poetry run coverage html env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - - - name: Coveralls GitHub Action - if: matrix.python-version == '3.11' - uses: coverallsapp/github-action@v2.2.3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/test_docs.yml b/.github/workflows/test_docs.yml index 8e4124922..3ac962b92 100644 --- a/.github/workflows/test_docs.yml +++ b/.github/workflows/test_docs.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - uses: actions/checkout@v2 @@ -22,8 +22,8 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - cache: 'poetry' - + cache: "poetry" + - name: Cache Poetry virtualenv uses: actions/cache@v2 with: @@ -33,9 +33,9 @@ jobs: ${{ runner.os }}-poetry- - name: Install dependencies - run: poetry install --with dev,docs,test-docs + run: poetry install --with dev,docs,test-docs,anthropic - name: Run tests - run: poetry run pytest tests/openai/docs + run: poetry run pytest tests/llm/test_openai/docs env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} \ No newline at end of file + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/.gitignore b/.gitignore index 613bf6b2a..a3e394bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +.vscode/ + examples/citation_with_extraction/fly.toml my_cache_directory/ tutorials/wandb/* diff --git a/.mypy.ini b/.mypy.ini deleted file mode 100644 index 66e31fb24..000000000 --- a/.mypy.ini +++ /dev/null @@ -1,29 +0,0 @@ -# Reference: -# https://github.com/openai/openai-python/blob/main/mypy.ini -# https://github.com/pytorch/pytorch/blob/main/mypy.ini -[mypy] -pretty=True -show_error_codes=True -python_version=3.9 - -strict_equality=True -implicit_reexport=True -check_untyped_defs=True -no_implicit_optional=True - -warn_return_any=False -warn_unreachable=True -warn_unused_configs=True - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores=False -warn_redundant_casts=False - -disallow_any_generics=True -disallow_untyped_defs=True -disallow_untyped_calls=True -disallow_subclassing_any=True -disallow_incomplete_defs=True -disallow_untyped_decorators=True -cache_fine_grained=True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de15eb2dc..6ea6ed731 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,16 +8,7 @@ repos: files: ^(instructor|tests|examples)/ - id: ruff-format # Run the formatter. name: Run Formatter (Ruff) - - repo: local + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.360 hooks: - - id: ci_type_mypy - name: Run Type Check (Mypy) - entry: > - bash -c 'set -o pipefail; - export CUSTOM_PACKAGES="instructor/_types/_alias.py instructor/cli/cli.py instructor/cli/files.py instructor/cli/usage.py instructor/exceptions.py" && - export CUSTOM_FLAGS="--python-version=3.9 --color-output --no-pretty --follow-imports=skip" && - curl -sSL https://raw.githubusercontent.com/gao-hongnan/omniverse/2fd5de1b8103e955cd5f022ab016b72fa901fa8f/scripts/devops/continuous-integration/type_mypy.sh | - bash' - language: system - types: [python] - pass_filenames: false + - id: pyright diff --git a/.ruff.toml b/.ruff.toml index e6a022d01..ccf250dbf 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -39,6 +39,8 @@ select = [ "E722", # unused arguments "ARG", + # pyupgrade + "UP", ] ignore = [ # mutable defaults @@ -53,10 +55,9 @@ unfixable = [ ] ignore-init-module-imports = true -[extend-per-file-ignores] +[lint.extend-per-file-ignores] "instructor/distil.py" = ["ARG002"] "tests/test_distil.py" = ["ARG001"] "tests/test_patch.py" = ["ARG001"] "examples/task_planner/task_planner_topological_sort.py" = ["ARG002"] "examples/citation_with_extraction/main.py" = ["ARG001"] - diff --git a/README.md b/README.md index 3e40932a1..77c97b160 100644 --- a/README.md +++ b/README.md @@ -1,289 +1,346 @@ -# Instructor +# Instructor: Structured LLM Outputs -_Structured outputs powered by llms. Designed for simplicity, transparency, and control._ - ---- +Instructor is a Python library that makes it a breeze to work with structured outputs from large language models (LLMs). Built on top of Pydantic, it provides a simple, transparent, and user-friendly API to manage validation, retries, and streaming responses. Get ready to supercharge your LLM workflows! [![Twitter Follow](https://img.shields.io/twitter/follow/jxnlco?style=social)](https://twitter.com/jxnlco) [![Discord](https://img.shields.io/discord/1192334452110659664?label=discord)](https://discord.gg/CV8sPM5k5Y) [![Downloads](https://img.shields.io/pypi/dm/instructor.svg)](https://pypi.python.org/pypi/instructor) -Instructor stands out for its simplicity, transparency, and user-centric design. We leverage Pydantic to do the heavy lifting, and we've built a simple, easy-to-use API on top of it by helping you manage [validation context](./docs/concepts/reask_validation.md), retries with [Tenacity](./docs/concepts/retrying.md), and streaming [Lists](./docs/concepts/lists.md) and [Partial](./docs/concepts/partial.md) responses. -Check us out in [Typescript](https://instructor-ai.github.io/instructor-js/), [Elixir](https://github.com/thmsmlr/instructor_ex/) and [PHP](https://github.com/cognesy/instructor-php/). +## Key Features -Instructor is not limited to the OpenAI API, we have support for many other backends that via patching. Check out more on [patching](./docs/concepts/patching.md). +- **Response Models**: Specify Pydantic models to define the structure of your LLM outputs +- **Retry Management**: Easily configure the number of retry attempts for your requests +- **Validation**: Ensure LLM responses conform to your expectations with Pydantic validation +- **Streaming Support**: Work with Lists and Partial responses effortlessly +- **Flexible Backends**: Seamlessly integrate with various LLM providers beyond OpenAI -1. Wrap OpenAI's SDK -2. Wrap the create method +## Get Started in Minutes -Including but not limited to: +Install Instructor with a single command: -- [Together](./docs/hub/together.md) -- [Ollama](./docs/hub/ollama.md) -- [AnyScale](./docs/hub/anyscale.md) -- [llama-cpp-python](./docs/hub/llama-cpp-python.md) +```bash +pip install -U instructor +``` -## Get Started in Moments +Now, let's see Instructor in action with a simple example: -Installing Instructor is a breeze. Simply run `pip install instructor` in your terminal and you're on your way to a smoother data handling experience! +```python +import instructor +from pydantic import BaseModel +from openai import OpenAI -## How Instructor Enhances Your Workflow -Our `instructor.patch` for the `OpenAI` class introduces three key enhancements: +# Define your desired output structure +class UserInfo(BaseModel): + name: str + age: int -- **Response Mode:** Specify a Pydantic model to streamline data extraction. -- **Max Retries:** Set your desired number of retry attempts for requests. -- **Validation Context:** Provide a context object for enhanced validator access. A Glimpse into Instructor's Capabilities. -### Using Validators +# Patch the OpenAI client +client = instructor.from_openai(OpenAI()) -To learn more about validators, checkout our blog post [Good LLM validation is just good validation](https://jxnl.github.io/instructor/blog/2023/10/23/good-llm-validation-is-just-good-validation/) +# Extract structured data from natural language +user_info = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserInfo, + messages=[{"role": "user", "content": "John Doe is 30 years old."}], +) -## Usage +print(user_info.name) +#> John Doe +print(user_info.age) +#> 30 +``` -With Instructor, your code becomes more efficient and readable. Here’s a quick peek: +### Using Anthropic Models -```py hl_lines="5 13" +```python import instructor -from openai import OpenAI +from anthropic import Anthropic from pydantic import BaseModel -# Enables `response_model` -client = instructor.patch(OpenAI()) - -class UserDetail(BaseModel): +class User(BaseModel): name: str age: int -user = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetail, +client = instructor.from_anthropic(Anthropic()) + +# note that client.chat.completions.create will also work +resp = client.messages.create( + model="claude-3-opus-20240229", + max_tokens=1024, messages=[ - {"role": "user", "content": "Extract Jason is 25 years old"}, + { + "role": "user", + "content": "Extract Jason is 25 years old.", + } ], + response_model=User, ) -assert isinstance(user, UserDetail) -assert user.name == "Jason" -assert user.age == 25 +assert isinstance(resp, User) +assert resp.name == "Jason" +assert resp.age == 25 ``` -## Primitive Types (str, int, float, bool) +### Using Cohere Models + +Make sure to install `cohere` and set your system environment variable with `export CO_API_KEY=`. + +``` +pip install cohere +``` ```python import instructor -import openai +import cohere +from pydantic import BaseModel + + +class User(BaseModel): + name: str + age: int + -client = instructor.patch(openai.OpenAI()) +client = instructor.from_cohere(cohere.Client()) -# Response model with simple types like str, int, float, bool +# note that client.chat.completions.create will also work resp = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=bool, + model="command-r-plus", + max_tokens=1024, messages=[ { "role": "user", - "content": "Is it true that Paris is the capital of France?", - }, + "content": "Extract Jason is 25 years old.", + } ], + response_model=User, ) -assert resp is True, "Paris is the capital of France" -print(resp) -#> True + +assert isinstance(resp, User) +assert resp.name == "Jason" +assert resp.age == 25 ``` -### Using async clients -For async clients you must use `apatch` vs. `patch`, as shown: +### Using Litellm -```py +```python import instructor -import asyncio -import openai +from litellm import completion from pydantic import BaseModel -aclient = instructor.apatch(openai.AsyncOpenAI()) - -class UserExtract(BaseModel): +class User(BaseModel): name: str age: int -task = aclient.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserExtract, +client = instructor.from_litellm(completion) + +resp = client.chat.completions.create( + model="claude-3-opus-20240229", + max_tokens=1024, messages=[ - {"role": "user", "content": "Extract jason is 25 years old"}, + { + "role": "user", + "content": "Extract Jason is 25 years old.", + } ], + response_model=User, ) - -response = asyncio.run(task) -print(response.model_dump_json(indent=2)) -""" -{ - "name": "Jason", - "age": 25 -} -""" +assert isinstance(resp, User) +assert resp.name == "Jason" +assert resp.age == 25 ``` -### Step 1: Patch the client +## Type are inferred correctly + +This was the dream of instructor but due to the patching of openai, it wasnt possible for me to get typing to work well. Now, with the new client, we can get typing to work well! We've also added a few `create_*` methods to make it easier to create iterables and partials, and to access the original completion. -First, import the required libraries and apply the `patch` function to the OpenAI module. This exposes new functionality with the `response_model` parameter. +### Calling `create` ```python +import openai import instructor -from openai import OpenAI +from pydantic import BaseModel -# This enables response_model keyword -# from client.chat.completions.create -client = instructor.patch(OpenAI()) + +class User(BaseModel): + name: str + age: int + + +client = instructor.from_openai(openai.OpenAI()) + +user = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) ``` -### Step 2: Define the Pydantic Model +Now if you use a IDE, you can see the type is correctly inferred. + +![type](./docs/blog/posts/img/type.png) -Create a Pydantic model to define the structure of the data you want to extract. This model will map directly to the information in the prompt. +### Handling async: `await create` + +This will also work correctly with asynchronous clients. ```python +import openai +import instructor from pydantic import BaseModel -class UserDetail(BaseModel): +client = instructor.from_openai(openai.AsyncOpenAI()) + + +class User(BaseModel): name: str age: int + + +async def extract(): + return await client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, + ) ``` -### Step 3: Extract +Notice that simply because we return the `create` method, the `extract()` function will return the correct user type. + +![async](./docs/blog/posts/img/async_type.png) -Use the `client.chat.completions.create` method to send a prompt and extract the data into the Pydantic object. The `response_model` parameter specifies the Pydantic model to use for extraction. It is helpful to annotate the variable with the type of the response model which will help your IDE provide autocomplete and spell check. +### Returning the original completion: `create_with_completion` + +You can also return the original completion object ```python -import instructor import openai +import instructor from pydantic import BaseModel -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) -class UserDetail(BaseModel): + +class User(BaseModel): name: str age: int -user = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetail, +user, completion = client.chat.completions.create_with_completion( + model="gpt-4-turbo-preview", messages=[ - {"role": "user", "content": "Extract Jason is 25 years old"}, + {"role": "user", "content": "Create a user"}, ], + response_model=User, ) - -assert isinstance(user, UserDetail) -assert user.name == "Jason" -assert user.age == 25 -print(user.model_dump_json(indent=2)) -""" -{ - "name": "Jason", - "age": 25 -} -""" ``` -## Pydantic Validation +![with_completion](./docs/blog/posts/img/with_completion.png) + -Validation can also be plugged into the same Pydantic model. +### Streaming Partial Objects: `create_partial` -In this example, if the answer attribute contains content that violates the rule "Do not say objectionable things", Pydantic will raise a validation error. +In order to handle streams, we still support `Iterable[T]` and `Partial[T]` but to simply the type inference, we've added `create_iterable` and `create_partial` methods as well! -```python hl_lines="9 15" -from pydantic import BaseModel, ValidationError, BeforeValidator -from typing_extensions import Annotated -from instructor import llm_validator +```python +import openai +import instructor +from pydantic import BaseModel -class QuestionAnswer(BaseModel): - question: str - answer: Annotated[ - str, BeforeValidator(llm_validator("don't say objectionable things")) - ] +client = instructor.from_openai(openai.OpenAI()) -try: - qa = QuestionAnswer( - question="What is the meaning of life?", - answer="The meaning of life is to be evil and steal", - ) -except ValidationError as e: - print(e) - """ - 1 validation error for QuestionAnswer - answer - Assertion failed, The statement promotes objectionable behavior by encouraging evil and stealing, which goes against the rule of not saying objectionable things. [type=assertion_error, input_value='The meaning of life is to be evil and steal', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/assertion_error - """ -``` +class User(BaseModel): + name: str + age: int -It is important to note here that the **error message is generated by the LLM**, not the code. Thus, it is helpful for re-asking the model. -```plaintext -1 validation error for QuestionAnswer -answer - Assertion failed, The statement is objectionable. (type=assertion_error) +user_stream = client.chat.completions.create_partial( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) + +for user in user_stream: + print(user) + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=25 + #> name=None age=25 + #> name=None age=25 + #> name=None age=25 + #> name=None age=25 + #> name=None age=25 + #> name='John Doe' age=25 + # name=None age=None + # name='' age=None + # name='John' age=None + # name='John Doe' age=None + # name='John Doe' age=30 ``` -## Re-ask on validation error +Notice now that the type inferred is `Generator[User, None]` -Here, the `UserDetails` model is passed as the `response_model`, and `max_retries` is set to 2. +![generator](./docs/blog/posts/img/generator.png) + +### Streaming Iterables: `create_iterable` + +We get an iterable of objects when we want to extract multiple objects. ```python +import openai import instructor +from pydantic import BaseModel -from openai import OpenAI -from pydantic import BaseModel, field_validator -# Apply the patch to the OpenAI client -client = instructor.patch(OpenAI()) +client = instructor.from_openai(openai.OpenAI()) -class UserDetails(BaseModel): +class User(BaseModel): name: str age: int - @field_validator("name") - @classmethod - def validate_name(cls, v): - if v.upper() != v: - raise ValueError("Name must be in uppercase.") - return v - -model = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetails, - max_retries=2, +users = client.chat.completions.create_iterable( + model="gpt-4-turbo-preview", messages=[ - {"role": "user", "content": "Extract jason is 25 years old"}, + {"role": "user", "content": "Create 2 users"}, ], + response_model=User, ) -print(model.model_dump_json(indent=2)) -""" -{ - "name": "JASON", - "age": 25 -} -""" +for user in users: + print(user) + #> name='John' age=30 + #> name='Jane' age=25 + # User(name='John Doe', age=30) + # User(name='Jane Smith', age=25) ``` -## [Evals](https://github.com/jxnl/instructor/tree/main/tests/openai/evals) +![iterable](./docs/blog/posts/img/iterable.png) + +## [Evals](https://github.com/jxnl/instructor/tree/main/tests/llm/test_openai/evals#how-to-contribute-writing-and-running-evaluation-tests) -We invite you to contribute to evals in `pytest` as a way to monitor the quality of the OpenAI models and the `instructor` library. To get started check out the [jxnl/instructor/tests/evals](https://github.com/jxnl/instructor/tree/main/tests/openai/evals) and contribute your own evals in the form of pytest tests. These evals will be run once a week and the results will be posted. +We invite you to contribute to evals in `pytest` as a way to monitor the quality of the OpenAI models and the `instructor` library. To get started check out the evals for [anthropic](https://github.com/jxnl/instructor/blob/main/tests/llm/test_anthropic/evals/test_simple.py) and [OpenAI](https://github.com/jxnl/instructor/tree/main/tests/llm/test_openai/evals#how-to-contribute-writing-and-running-evaluation-tests) and contribute your own evals in the form of pytest tests. These evals will be run once a week and the results will be posted. ## Contributing diff --git a/build_mkdocs.sh b/build_mkdocs.sh new file mode 100644 index 000000000..1655b14ec --- /dev/null +++ b/build_mkdocs.sh @@ -0,0 +1,3 @@ +pip install -r requirements.txt +pip install -r requirements-doc.txt +mkdocs build diff --git a/docs/api.md b/docs/api.md index 01e24954a..7085b13e8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # API Reference -::: instructor.patch +::: instructor.from_openai ::: instructor.dsl.validators diff --git a/docs/blog/posts/anthropic.md b/docs/blog/posts/anthropic.md new file mode 100644 index 000000000..b2966b27f --- /dev/null +++ b/docs/blog/posts/anthropic.md @@ -0,0 +1,70 @@ +--- +draft: False +date: 2024-03-20 +authors: + - jxnl +--- + +# Announcing Anthropic Support + +A special shoutout to [Shreya](https://twitter.com/shreyaw_) for her contributions to the anthropic support. As of now, all features are operational with the exception of streaming support. + +For those eager to experiment, simply patch the client with `ANTHROPIC_JSON`, which will enable you to leverage the `anthropic` client for making requests. + +``` +pip install instructor[anthropic] +``` + +!!! warning "Missing Features" + + Just want to acknowledge that we know that we are missing partial streaming and some better re-asking support for XML. We are working on it and will have it soon. + +```python +from pydantic import BaseModel +from typing import List +import anthropic +import instructor + +# Patching the Anthropics client with the instructor for enhanced capabilities +anthropic_client = instructor.from_openai( + create=anthropic.Anthropic().messages.create, + mode=instructor.Mode.ANTHROPIC_JSON +) + +class Properties(BaseModel): + name: str + value: str + +class User(BaseModel): + name: str + age: int + properties: List[Properties] + +user_response = anthropic_client( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name, age, and properties.", + } + ], + response_model=User, +) # type: ignore + +print(user_response.model_dump_json(indent=2)) +""" +{ + "name": "John", + "age": 25, + "properties": [ + { + "key": "favorite_color", + "value": "blue" + } + ] +} +``` + +We're encountering challenges with deeply nested types and eagerly invite the community to test, provide feedback, and suggest necessary improvements as we enhance the anthropic client's support. \ No newline at end of file diff --git a/docs/blog/posts/best_framework.md b/docs/blog/posts/best_framework.md new file mode 100644 index 000000000..f64091de2 --- /dev/null +++ b/docs/blog/posts/best_framework.md @@ -0,0 +1,79 @@ +--- +draft: False +date: 2024-03-05 +slug: zero-cost-abstractions +tags: + - python + - llms +authors: + - jxnl +--- + +# Why Instructor is the Best Library for Structured LLM Outputs + +Large language models (LLMs) like GPTs are incredibly powerful, but working with their open-ended text outputs can be challenging. This is where the Instructor library shines - it allows you to easily map LLM outputs to structured data using Python type annotations. + + + +The core idea behind Instructor is incredibly simple: it's just a patch over the OpenAI Python SDK that adds a response_model parameter. This parameter lets you pass in a Pydantic model that describes the structure you want the LLM output mapped to. Pydantic models are defined using standard Python type hints, so there's zero new syntax to learn. + +Here's an example of extracting structured user data from an LLM: + +```python +from pydantic import BaseModel +import instructor + +class User(BaseModel): + name: str + age: int + +client = instructor.from_openai(openai.OpenAI()) + +user = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=User, # (1)! + messages=[ + { + "role": "user", + "content": "Extract the user's name and age from this: John is 25 years old" + } + ] +) + +print(user) # (2)! +# > User(name='John', age=25) +``` + +1. Notice that now we have a new response_model parameter that we pass in to the completions.create method. This parameter lets us specify the structure we want the LLM output to be mapped to. In this case, we're using a Pydantic model called User that describes a user's name and age. +2. The output of the completions.create method is a User object that matches the structure we specified in the response_model parameter, rather than a ChatCompletion. + +## Other Features + +Other features on instructor, in and out of the llibrary are: + +1. Ability to use [Tenacity in retrying logic](../../concepts/retrying.md) +2. Ability to use [Pydantic's validation context](../../concepts/reask_validation.md) +3. [Parallel Tool Calling](../../concepts/parallel.md) with correct types +4. Streaming [Partial](../../concepts/partial.md) and [Iterable](../../concepts/iterable.md) data. +5. Returning [Primitive](../../concepts/types.md) Types and [Unions](../../concepts/unions.md) as well! +6. Lots, and Lots of [Cookbooks](../../examples/index.md), [Tutorials](../../tutorials/1-introduction.ipynb), Documentation and even [instructor hub](../../hub/index.md) + +## Instructor's Broad Applicability + +One of the key strengths of Instructor is that it's designed as a lightweight patch over the official OpenAI Python SDK. This means it can be easily integrated not just with OpenAI's hosted API service, but with any provider or platform that exposes an interface compatible with the OpenAI SDK. + +For example, providers like [Anyscale](../../hub/anyscale.md), [Together](../../hub/together.md), [Ollama](../../hub/ollama.md), [Groq](../../hub/groq.md), and [llama-cpp-python](../../hub/llama-cpp-python.md) all either use or mimic the OpenAI Python SDK under the hood. With Instructor's zero-overhead patching approach, teams can immediately start deriving structured data outputs from any of these providers. There's no need for custom integration work. + +## Direct access to the messages array + +Unlike other libraries that abstract away the `messages=[...]` parameter, Instructor provides direct access. This direct approach facilitates intricate prompt engineering, ensuring compatibility with OpenAI's evolving message types, including future support for images, audio, or video, without the constraints of string formatting. + +## Low Abstraction + +What makes Instructor so powerful is how seamlessly it integrates with existing OpenAI SDK code. To use it, you literally just call instructor.from_openai() on your OpenAI client instance, then use response_model going forward. There's no complicated refactoring or new abstractions to wrap your head around. + +This incremental, zero-overhead adoption path makes Instructor perfect for sprinkling structured LLM outputs into an existing OpenAI-based application. You can start extracting data models from simple prompts, then incrementally expand to more complex hierarchical models, streaming outputs, and custom validations. + +And if you decide Instructor isn't a good fit after all, removing it is as simple as not applying the patch! The familiarity and flexibility of working directly with the OpenAI SDK is a core strength. + +Instructor solves the "string hellll" of unstructured LLM outputs. It allows teams to easily realize the full potential of tools like GPTs by mapping their text to type-safe, validated data structures. If you're looking to get more structured value out of LLMs, give Instructor a try! diff --git a/docs/blog/posts/caching.md b/docs/blog/posts/caching.md index 3a502ea1e..9f22c7b39 100644 --- a/docs/blog/posts/caching.md +++ b/docs/blog/posts/caching.md @@ -28,7 +28,7 @@ from openai import OpenAI from pydantic import BaseModel # Enables `response_model` -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserDetail(BaseModel): @@ -177,7 +177,7 @@ import diskcache from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) cache = diskcache.Cache('./my_cache_directory') @@ -281,7 +281,7 @@ import instructor from pydantic import BaseModel from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) cache = redis.Redis("localhost") diff --git a/docs/blog/posts/chain-of-density.md b/docs/blog/posts/chain-of-density.md index bb02fb811..a549c6f3a 100644 --- a/docs/blog/posts/chain-of-density.md +++ b/docs/blog/posts/chain-of-density.md @@ -286,7 +286,7 @@ Now that we have our models and the rough flow figured out, let's implement a fu from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) #(1)! +client = instructor.from_openai(OpenAI()) #(1)! def summarize_article(article: str, summary_steps: int = 3): summary_chain = [] @@ -397,7 +397,7 @@ import instructor from pydantic import BaseModel from openai import OpenAI -client = instructor.patch(OpenAI()) # (2)! +client = instructor.from_openai(OpenAI()) # (2)! logging.basicConfig(level=logging.INFO) #(3)! diff --git a/docs/blog/posts/citations.md b/docs/blog/posts/citations.md index def94272f..70976ce0b 100644 --- a/docs/blog/posts/citations.md +++ b/docs/blog/posts/citations.md @@ -32,7 +32,7 @@ from openai import OpenAI from pydantic import BaseModel, ValidationInfo, field_validator import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Statements(BaseModel): diff --git a/docs/blog/posts/fake-data.md b/docs/blog/posts/fake-data.md new file mode 100644 index 000000000..e97bfd50c --- /dev/null +++ b/docs/blog/posts/fake-data.md @@ -0,0 +1,193 @@ +--- +draft: False +date: 2024-03-08 +authors: + - jxnl +--- + +# Simple Synthetic Data Generation + +What that people have been using instructor for is to generate synthetic data rather than extracting data itself. We can even use the J-Schemo extra fields to give specific examples to control how we generate data. + +Consider the example below. We'll likely generate very simple names. + +```python +from typing import Iterable +from pydantic import BaseModel +import instructor +from openai import OpenAI + + +# Define the UserDetail model +class UserDetail(BaseModel): + name: str + age: int + + +# Patch the OpenAI client to enable the response_model functionality +client = instructor.from_openai(OpenAI()) + + +def generate_fake_users(count: int) -> Iterable[UserDetail]: + return client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + messages=[ + {"role": "user", "content": f"Generate a {count} synthetic users"}, + ], + ) + + +for user in generate_fake_users(5): + print(user) + """ + name='Alice' age=25 + name='Bob' age=30 + name='Charlie' age=35 + name='David' age=40 + name='Eve' age=45 + """ +``` + +## Leveraging Simple Examples + +We might want to set examples as part of the prompt by leveraging Pydantics configuration. We can set examples directly in the JSON scheme itself. + +```python +from typing import Iterable +from pydantic import BaseModel +import instructor +from openai import OpenAI + + +# Define the UserDetail model +class UserDetail(BaseModel): + name: str = Field(examples=["Timothee Chalamet", "Zendaya"]) + age: int + + +# Patch the OpenAI client to enable the response_model functionality +client = instructor.from_openai(OpenAI()) + + +def generate_fake_users(count: int) -> Iterable[UserDetail]: + return client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + messages=[ + {"role": "user", "content": f"Generate a {count} synthetic users"}, + ], + ) + + +for user in generate_fake_users(5): + print(user) + """ + name='Timothee Chalamet' age=25 + name='Zendaya' age=24 + name='Keanu Reeves' age=56 + name='Scarlett Johansson' age=36 + name='Chris Hemsworth' age=37 + """ +``` + +By incorporating names of celebrities as examples, we have shifted towards generating synthetic data featuring well-known personalities, moving away from the simplistic, single-word names previously used. + +## Leveraging Complex Example + +To effectively generate synthetic examples with more nuance, lets upgrade to the "gpt-4-turbo-preview" model, use model level examples rather than attribute level examples: + +```Python +import instructor + +from typing import Iterable +from pydantic import BaseModel, Field, ConfigDict +from openai import OpenAI + + +# Define the UserDetail model +class UserDetail(BaseModel): + """Old Wizards""" + name: str + age: int + + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + {"name": "Gandalf the Grey", "age": 1000}, + {"name": "Albus Dumbledore", "age": 150}, + ] + } + ) + + +# Patch the OpenAI client to enable the response_model functionality +client = instructor.from_openai(OpenAI()) + + +def generate_fake_users(count: int) -> Iterable[UserDetail]: + return client.chat.completions.create( + model="gpt-4-turbo-preview", + response_model=Iterable[UserDetail], + messages=[ + {"role": "user", "content": f"Generate `{count}` synthetic examples"}, + ], + ) + + +for user in generate_fake_users(5): + print(user) + """ + name='Merlin' age=196 + name='Saruman the White' age=543 + name='Radagast the Brown' age=89 + name='Morgoth' age=901 + name='Filius Flitwick' age=105 + """ +``` + +## Leveraging Descriptions + +By adjusting the descriptions within our Pydantic models, we can subtly influence the nature of the synthetic data generated. This method allows for a more nuanced control over the output, ensuring that the generated data aligns more closely with our expectations or requirements. + +For instance, specifying "Fancy French sounding names" as a description for the `name` field in our `UserDetail` model directs the generation process to produce names that fit this particular criterion, resulting in a dataset that is both diverse and tailored to specific linguistic characteristics. + + +```python +import instructor + +from typing import Iterable +from pydantic import BaseModel, Field +from openai import OpenAI + + +# Define the UserDetail model +class UserDetail(BaseModel): + name: str = Field(description="Fancy French sounding names") + age: int + + +# Patch the OpenAI client to enable the response_model functionality +client = instructor.from_openai(OpenAI()) + + +def generate_fake_users(count: int) -> Iterable[UserDetail]: + return client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + messages=[ + {"role": "user", "content": f"Generate `{count}` synthetic users"}, + ], + ) + + +for user in generate_fake_users(5): + print(user) + """ + name='Jean' age=25 + name='Claire' age=30 + name='Pierre' age=22 + name='Marie' age=27 + name='Luc' age=35 + """ +``` \ No newline at end of file diff --git a/docs/blog/posts/full-fastapi-visibility.md b/docs/blog/posts/full-fastapi-visibility.md new file mode 100644 index 000000000..40eff8b08 --- /dev/null +++ b/docs/blog/posts/full-fastapi-visibility.md @@ -0,0 +1,412 @@ +--- +draft: False +date: 2024-05-03 +slug: fastapi-open-telemetry-and-instructor +tags: + - Logfire + - Open Telemetry + - FastAPI +authors: + - ivanleomk + - jxnl +--- + +# Why Logfire is a perfect fit for FastAPI + Instructor + +Logfire is a new tool that provides key insight into your application with Open Telemtry. Instead of using ad-hoc print statements, Logfire helps to profile every part of your application and is integrated directly into Pydantic and FastAPI, two popular libraries amongst Instructor users. + +In short, this is the secret sauce to help you get your application to the finish line and beyond. We'll show you how to easily integrate Logfire into FastAPI, one of the most popular choices amongst users of Instructor using two examples + +1. Data Extraction from a single User Query +2. Using `asyncio` to process multiple users in parallel +3. Streaming multiple objects using an `Iterable` so that they're avaliable on demand + + + +As usual, all of the code that we refer to here is provided in [examples/logfire-fastapi](https://www.github.com/jxnl/instructor/tree/main/examples/logfire-fastapi) for you to use in your projects. + +??? info "Configure Logfire" + + Before starting this tutorial, make sure that you've registered for a [Logfire](https://logfire.pydantic.dev/) account. You'll also need to create a project to track these logs. Lastly, in order to see the request body, you'll also need to configure the default log level to `debug` instead of the default `info` on the dashboard console. + +Make sure to create a virtual environment and install all of the packages inside the `requirements.txt` file at [examples/logfire-fastapi](https://www.github.com/jxnl/instructor/tree/main/examples/logfire-fastapi). + +## Data Extraction + +Let's start by trying to extract some user information given a user query. We can do so with a simple Pydantic model as seen below. + +```python +from pydantic import BaseModel +from fastapi import FastAPI +from openai import AsyncOpenAI +import instructor + + +class UserData(BaseModel): + query: str + + +class UserDetail(BaseModel): + name: str + age: int + + +app = FastAPI() +client = instructor.from_openai(AsyncOpenAI()) + + +@app.post("/user", response_model=UserDetail) +async def endpoint_function(data: UserData) -> UserDetail: + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{data.query}`"}, + ], + ) + + return user_detail + +``` + +This simple endpoint takes in a user query and extracts out a user from the statement. Let's see how we can add in Logfire into this endpoint with just a few lines of code + +```python hl_lines="5 18-21" +from pydantic import BaseModel +from fastapi import FastAPI +from openai import AsyncOpenAI +import instructor +import logfire #(1)! + + +class UserData(BaseModel): + query: str + + +class UserDetail(BaseModel): + name: str + age: int + + +app = FastAPI() +openai_client = AsyncOpenAI() #(2)! +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +logfire.instrument_fastapi(app) +client = instructor.from_openai(openai_client) + + +@app.post("/user", response_model=UserDetail) +async def endpoint_function(data: UserData) -> UserDetail: + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{data.query}`"}, + ], + ) + + return user_detail +``` + +1. Import in the logfire package +2. Setup logging using their native integrations with FastAPI and OpenAI + +With just those few lines of code, we've got ourselves a working integration with Logfire. When we call our endpoint at `/user` with the following payload, everything is immediately logged in the console. + +```bash +curl -X 'POST' \ + 'http://localhost:8000/user' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "query": "Daniel is a 24 year man living in New York City" +}' +``` + +We can see that Pydantic has nicely logged for us the validation result of our openai call here. Just right above, we also have the result of the OpenAI call. + +![Pydantic Validation](img/logfire-sync-pydantic-validation.png) + +We've also got full visibility into the arguments that were passed into the endpoint when we called it. This is extremely useful for users when they eventually want to reproduce errors in production locally. + +![FastAPI arguments](img/logfire-sync-fastapi-arguments.png) + +## Using Asyncio + +Sometimes, we might need to run multiple jobs in parallel. Let's see how we can take advantage of `asyncio` so that we can speed up our operations. We can do so by adding the following bits of code to our previous file. + +??? info "What is Asyncio?" + + For a deeper guide into how to work with Asycnio, see our previous guide [here](./learn-async.md). + +=== "New Code" + + ```python + import asyncio + + class MultipleUserData(BaseModel): + queries: list[str] + + @app.post("/many-users", response_model=list[UserDetail]) + async def extract_many_users(data: MultipleUserData): + async def extract_user(query: str): + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + coros = [extract_user(query) for query in data.queries] + return await asyncio.gather(*coros) + ``` + +=== "Full File" + + ```python + from pydantic import BaseModel + from fastapi import FastAPI + from openai import AsyncOpenAI + import instructor + import logfire + from collections.abc import Iterable + from fastapi.responses import StreamingResponse + import asyncio + + + class UserData(BaseModel): + query: str + + + class MultipleUserData(BaseModel): + queries: list[str] + + + class UserDetail(BaseModel): + name: str + age: int + + + app = FastAPI() + openai_client = AsyncOpenAI() + logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) + logfire.instrument_openai(openai_client) + logfire.instrument_fastapi(app) + client = instructor.from_openai(openai_client) + + + @app.post("/user", response_model=UserDetail) + async def endpoint_function(data: UserData) -> UserDetail: + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{data.query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + + @app.post("/many-users", response_model=list[UserDetail]) + async def extract_many_users(data: MultipleUserData): + async def extract_user(query: str): + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + coros = [extract_user(query) for query in data.queries] + return await asyncio.gather(*coros) + ``` + +We can call this endpoint with a simple `curl` call + +```bash +curl -X 'POST' \ + 'http://localhost:8000/many-users' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "queries": [ + "Daniel is a 34 year man in New York City","Sarah is a 20 year old living in Tokyo", "Jeffrey is 55 and lives down in Leeds" + ] +}' +``` + +This is all logged in Logfire as seen below. We have complete visiblity into the eprformance of our entire application and it's pretty clear that a large chunk of the latency is taken up by the OpenAI Call. + +We could also potentially separate the logs into more graunular levels by creating a new span for each instance of `extract_user` created. + +![Logfire Asyncio](img/logfire-asyncio.png) + +## Streaming + +Now let's see how we can take advantage of Instructor's `Iterable` support to stream multiple instances of an extracted object. This is extremely useful for application where speed is crucial and users want to get the results quickly. + +Let's add a new endpoint to our server to see how this might work + +=== "New Code" + + ```python + import asyncio + from collections.abc import Iterable + from fastapi.responses import StreamingResponse + + class MultipleUserData(BaseModel): + queries: list[str] + + @app.post("/extract", response_class=StreamingResponse) + async def extract(data: UserData): + supressed_client = AsyncOpenAI() + logfire.instrument_openai(supressed_client, suppress_other_instrumentation=False) #(1)! + client = instructor.from_openai(supressed_client) + users = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + stream=True, + messages=[ + {"role": "user", "content": data.query}, + ], + ) + + async def generate(): + with logfire.span("Generating User Response Objects"): + async for user in users: + resp_json = user.model_dump_json() + logfire.info("Returning user object", value=resp_json) + + yield resp_json + + return StreamingResponse(generate(), media_type="text/event-stream") + ``` + + 1. Note that we supress instrumentation to print out the stream objects. This has to do with the parsing of partials in Instructor. + +=== "Full File" + + ```python + from pydantic import BaseModel + from fastapi import FastAPI + from openai import AsyncOpenAI + import instructor + import logfire + import asyncio + from collections.abc import Iterable + from fastapi.responses import StreamingResponse + + + class UserData(BaseModel): + query: str + + + class MultipleUserData(BaseModel): + queries: list[str] + + + class UserDetail(BaseModel): + name: str + age: int + + + app = FastAPI() + openai_client = AsyncOpenAI() + logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) + logfire.instrument_fastapi(app) + logfire.instrument_openai(openai_client) + client = instructor.from_openai(openai_client) + + + @app.post("/user", response_model=UserDetail) + async def endpoint_function(data: UserData) -> UserDetail: + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{data.query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + + @app.post("/many-users", response_model=list[UserDetail]) + async def extract_many_users(data: MultipleUserData): + async def extract_user(query: str): + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + coros = [extract_user(query) for query in data.queries] + return await asyncio.gather(*coros) + + + @app.post("/extract", response_class=StreamingResponse) + async def extract(data: UserData): + supressed_client = AsyncOpenAI() + logfire.instrument_openai(supressed_client, suppress_other_instrumentation=False) + client = instructor.from_openai(supressed_client) + users = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + stream=True, + messages=[ + {"role": "user", "content": data.query}, + ], + ) + + async def generate(): + with logfire.span("Generating User Response Objects"): + async for user in users: + resp_json = user.model_dump_json() + logfire.info("Returning user object", value=resp_json) + + yield resp_json + + return StreamingResponse(generate(), media_type="text/event-stream") + ``` + +We can call and log out the stream returned using the `requests` library and using the `iter_content` method + +```python +import requests + +response = requests.post( + "http://127.0.0.1:3000/extract", + json={ + "query": "Alice and Bob are best friends. They are currently 32 and 43 respectively. " + }, + stream=True, +) + +for chunk in response.iter_content(chunk_size=1024): + if chunk: + print(str(chunk, encoding="utf-8"), end="\n") + +``` + +This gives us the output of + +```bash +{"name":"Alice","age":32} +{"name":"Bob","age":43} +``` + +We can also see the individual stream objects inside the Logfire dashboard as seen below. Note that we've grouped the generated logs inside a span of its own for easy logging. + +![Logfire Stream](img/logfire-stream.png) diff --git a/docs/blog/posts/generator.md b/docs/blog/posts/generator.md index 748c7f507..28b5c9d19 100644 --- a/docs/blog/posts/generator.md +++ b/docs/blog/posts/generator.md @@ -238,7 +238,7 @@ from openai import OpenAI from typing import Iterable from pydantic import BaseModel -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.function_calls.Mode.JSON) class ProductRecommendation(BaseModel): diff --git a/docs/blog/posts/img/async_type.png b/docs/blog/posts/img/async_type.png new file mode 100644 index 000000000..32080259c Binary files /dev/null and b/docs/blog/posts/img/async_type.png differ diff --git a/docs/blog/posts/img/classification-logfire.png b/docs/blog/posts/img/classification-logfire.png new file mode 100644 index 000000000..f64951435 Binary files /dev/null and b/docs/blog/posts/img/classification-logfire.png differ diff --git a/docs/blog/posts/img/downloads.png b/docs/blog/posts/img/downloads.png new file mode 100644 index 000000000..56447f6bf Binary files /dev/null and b/docs/blog/posts/img/downloads.png differ diff --git a/docs/blog/posts/img/generator.png b/docs/blog/posts/img/generator.png new file mode 100644 index 000000000..c3a6fa02b Binary files /dev/null and b/docs/blog/posts/img/generator.png differ diff --git a/docs/blog/posts/img/image-logfire.png b/docs/blog/posts/img/image-logfire.png new file mode 100644 index 000000000..89197e42d Binary files /dev/null and b/docs/blog/posts/img/image-logfire.png differ diff --git a/docs/blog/posts/img/iterable.png b/docs/blog/posts/img/iterable.png new file mode 100644 index 000000000..05b52b603 Binary files /dev/null and b/docs/blog/posts/img/iterable.png differ diff --git a/docs/blog/posts/img/logfire-asyncio.png b/docs/blog/posts/img/logfire-asyncio.png new file mode 100644 index 000000000..cc6160aab Binary files /dev/null and b/docs/blog/posts/img/logfire-asyncio.png differ diff --git a/docs/blog/posts/img/logfire-stream.png b/docs/blog/posts/img/logfire-stream.png new file mode 100644 index 000000000..175abf9c7 Binary files /dev/null and b/docs/blog/posts/img/logfire-stream.png differ diff --git a/docs/blog/posts/img/logfire-sync-fastapi-arguments.png b/docs/blog/posts/img/logfire-sync-fastapi-arguments.png new file mode 100644 index 000000000..4d879867c Binary files /dev/null and b/docs/blog/posts/img/logfire-sync-fastapi-arguments.png differ diff --git a/docs/blog/posts/img/logfire-sync-pydantic-validation.png b/docs/blog/posts/img/logfire-sync-pydantic-validation.png new file mode 100644 index 000000000..494960a76 Binary files /dev/null and b/docs/blog/posts/img/logfire-sync-pydantic-validation.png differ diff --git a/docs/blog/posts/img/statista-image.jpeg b/docs/blog/posts/img/statista-image.jpeg new file mode 100644 index 000000000..c3aa3b732 Binary files /dev/null and b/docs/blog/posts/img/statista-image.jpeg differ diff --git a/docs/blog/posts/img/type.png b/docs/blog/posts/img/type.png new file mode 100644 index 000000000..b65c51077 Binary files /dev/null and b/docs/blog/posts/img/type.png differ diff --git a/docs/blog/posts/img/validation-logfire.png b/docs/blog/posts/img/validation-logfire.png new file mode 100644 index 000000000..60e9abb6c Binary files /dev/null and b/docs/blog/posts/img/validation-logfire.png differ diff --git a/docs/blog/posts/img/with_completion.png b/docs/blog/posts/img/with_completion.png new file mode 100644 index 000000000..48121fdf1 Binary files /dev/null and b/docs/blog/posts/img/with_completion.png differ diff --git a/docs/blog/posts/introduction.md b/docs/blog/posts/introduction.md index 064a41791..91bf5c296 100644 --- a/docs/blog/posts/introduction.md +++ b/docs/blog/posts/introduction.md @@ -35,7 +35,7 @@ import instructor from openai import OpenAI # Enables the response_model -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserDetail(pydantic.BaseModel): diff --git a/docs/blog/posts/langsmith.md b/docs/blog/posts/langsmith.md index 40229bac8..3c740a22e 100644 --- a/docs/blog/posts/langsmith.md +++ b/docs/blog/posts/langsmith.md @@ -52,7 +52,7 @@ from enum import Enum client = wrap_openai(AsyncOpenAI()) # Patch the client with instructor -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) # Rate limit the number of requests sem = asyncio.Semaphore(5) diff --git a/docs/blog/posts/logfire.md b/docs/blog/posts/logfire.md new file mode 100644 index 000000000..0758a846c --- /dev/null +++ b/docs/blog/posts/logfire.md @@ -0,0 +1,280 @@ +--- +draft: False +date: 2024-05-01 +slug: instructor-logfire +tags: + - python + - observability + - open-telemetry +authors: + - ivanleomk + - jxnl +--- + +## Introduction + +Logfire is a new observability platform coming from the creators of Pydantic. It integrates almost seamlessly with many of your favourite libraries such as Pydantic, HTTPx and Instructor. In this article, we'll show you how to use Logfire with Instructor to gain visibility into the performance of your entire application. + +We'll walk through the following examples + +1. Classifying scam emails using Instructor +2. Performing simple validation using the `llm_validator` +3. Extracting data into a markdown table from an infographic with GPT4V + + + +As usual, all of the code that we refer to here is provided in [examples/logfire](https://www.github.com/jxnl/instructor/tree/main/examples/logfire) for you to use in your projects. + +- `classify.py`: Email Classification Example +- `image.py` : GPT4-V Example +- `validate.py` : `llm_validator` example + +??? info "Configure Logfire" + + Before starting this tutorial, make sure that you've registered for a [Logfire](https://logfire.pydantic.dev/) account. You'll also need to create a project to track these logs. + +We'll need to install our dependencies and configure logfire auth before proceeding so simply run the commands below. Logfire will handle the authentication and configuration of your project. + +```bash +pip install logfire openai instructor pydantic pandas tabulate +logfire auth +``` + +## Classification + +Now that we've got Logfire setup, let's see how we can get it to help us track a simple classification job. + +Logfire is dead simple to integrate - all it takes is 2 lines of code and we have it setup. + +```python +from pydantic import BaseModel +from openai import OpenAI +import instructor +import logfire + + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) #(1)! +logfire.instrument_openai(openai_client) #(2)! +client = instructor.from_openai(openai_client) +``` + +1. We add Pydantic logging using `logfire`. Note that depending on your use-case, you can configure what you want to log with Pydantic +2. We use their openai_integration to configure logging for our client before using instructor on it + +In this example, we'll be looking at classifying emails as either spam or not spam. To do so, we can define a simple Pydantic model as seen below. + +```python +import enum + +class Labels(str, enum.Enum): + """Enumeration for single-label text classification.""" + + SPAM = "spam" + NOT_SPAM = "not_spam" + + +class SinglePrediction(BaseModel): + """ + Class for a single class label prediction. + """ + + class_label: Labels +``` + +We can then use this in a generic instructor function as seen below that simply asks the model to classify text and return it in the form of a `SinglePrediction` Pydantic object. + +Logfire can help us to log this entire function, and what's happening inside it, even down to the model validation level by using their `logfire.instrument` decorator. + +```python +@logfire.instrument("classification", extract_args=True) #(1)! +def classify(data: str) -> SinglePrediction: + """Perform single-label classification on the input text.""" + return client.chat.completions.create( + model="gpt-3.5-turbo-0613", + response_model=SinglePrediction, + messages=[ + { + "role": "user", + "content": f"Classify the following text: {data}", + }, + ], + ) +``` + +1. Logfire allows us to use the `logfire.instrument` decorator and tag a function to a specific name. + +Let's see what happens when we run this against a list of different emails + +```python +emails = [ + "Hello there I'm a Nigerian prince and I want to give you money", + "Meeting with Thomas has been set at Friday next week", + "Here are some weekly product updates from our marketing team", +] + +for email in emails: + classify(email) +``` + +There are a few important things here that the logs immediately give us + +1. The duration that each individual portion of our code took to run +2. The payload that we sent over to OpenAI +3. The exact arguments and results that were passed to each individual portion of our code at each step + +![Logfire Classification](img/classification-logfire.png) + +## LLM Validators + +For our second example, we'll use the inbuilt `llm_validator` that instructor provides out of the box to validate that our statements don't contain unsafe content that we might not want to serve to users. Let's start by defining a simple Pydantic Model that can do so and configure our logfire integration. + +```python +from typing import Annotated +from pydantic import BaseModel, ValidationError +from pydantic.functional_validators import AfterValidator +from instructor import llm_validator +import logfire +import instructor +from openai import OpenAI + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +client = instructor.from_openai(openai_client) + + +class Statement(BaseModel): + message: Annotated[ + str, + AfterValidator( + llm_validator("Don't allow any objectionable content", client=client) + ), + ] +``` + +We can then test out our new validator with a few sample statements to see how our validator is working in practice. + +```python +messages = [ + "I think we should always treat violence as the best solution", + "There are some great pastries down the road at this bakery I know", +] + +for message in messages: + try: + Statement(message=message) + except ValidationError as e: + print(e) +``` + +With Logfire, we can capture the entirety of the validation proccess. As seen below, we have access to not only the original input data, but also the schema that was being used, the errors that were thrown and even the exact field that threw the error. + +![Logfire Validation](img/validation-logfire.png) + +## Vision Models + +For our last example, let's see how we can use Logfire to extract structured data from an image using GPT-4V with OpenAI. We'll be using a simple bar graph here and using `GPT4V` to extract the data from the image from statista below and convert it into a markdown format. + +![Reference Image](img/statista-image.jpeg) + +What we want is an output of the combined numbers as seen below + +| Country | Total Skier Visits (M) | +| :------------ | ---------------------: | +| United States | 55.5 | +| Austria | 43.6 | +| France | 40.7 | +| Japan | 26.6 | +| Italy | 22.3 | +| Switzerland | 22 | +| Canada | 18.5 | +| China | 17.9 | +| Sweden | 9.2 | +| Germany | 7 | + +This is relatively simple with Pydantic. What we need to do is to define a custom type which will handle the conversion process as seen below + +```python +from pydantic import BaseModel, Field, BeforeValidator, PlainSerializer, InstanceOf, WithJsonSchema + +def md_to_df(data: Any) -> Any: + # Convert markdown to DataFrame + if isinstance(data, str): + return ( + pd.read_csv( + StringIO(data), # Process data + sep="|", + index_col=1, + ) + .dropna(axis=1, how="all") + .iloc[1:] + .applymap(lambda x: x.strip()) + ) + return data + + +MarkdownDataFrame = Annotated[ + InstanceOf[pd.DataFrame], #(1)! + BeforeValidator(md_to_df), #(2)! + WithJsonSchema( #(3)! + { + "type": "string", + "description": "The markdown representation of the table, each one should be tidy, do not try to join tables that should be seperate", + } + ), +] +``` + +1. We indicate that the type of this type should be a pandas dataframe +2. We run a validation step to ensure that we can convert the input into a valid pandas dataframe and return a new pandas Dataframe for our model to use +3. We then override the type of the schema so that when we pass it to OpenAI, it knows to generate a table in a markdown format. + +We can then use this in a normal instructor call + +```python +import instructor +import logfire + + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +client = instructor.from_openai( + openai_client, mode=instructor.Mode.MD_JSON +) + +@logfire.instrument("extract-table", extract_args=True) +def extract_table_from_image(url: str) -> Iterable[Table]: + return client.chat.completions.create( + model="gpt-4-vision-preview", + response_model=Iterable[Table], + max_tokens=1800, + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Extract out a table from the image. Only extract out the total number of skiiers.", + }, + {"type": "image_url", "image_url": {"url": url}}, + ], + } + ], + ) +``` + +We can then call it as seen below + +```python +url = "https://cdn.statcdn.com/Infographic/images/normal/16330.jpeg" +tables = extract_table_from_image(url) +for table in tables: + print(table.caption, end="\n") + print(table.dataframe.to_markdown()) +``` + +Logfire is able to capture the stack track of the entire call as seen below, profile each part of our application and most importantly capture the raw inputs of the OpenAI call alongside any potential errors. + +![Logfire Image](img/image-logfire.png) diff --git a/docs/blog/posts/matching-language.md b/docs/blog/posts/matching-language.md new file mode 100644 index 000000000..c82e3324c --- /dev/null +++ b/docs/blog/posts/matching-language.md @@ -0,0 +1,259 @@ +--- +draft: False +date: 2024-03-28 +slug: matching-language-summaries +tags: + - multilingual + - summarization +authors: + - jxnl +--- + +# Matching Language in Multilingual Summarization Tasks + +When asking language models to summarize text, there's a risk that the generated summary ends up in English, even if the source text is in another language. This is likely due to the instructions being provided in English, biasing the model towards English output. + +In this post, we explore techniques to ensure the language of the generated summary matches the language of the source text. We leverage Pydantic for data validation and the `langdetect` library for language identification. + + + +## The Problem + +Consider the following example where we ask a language model to summarize text in various languages: + +```txt +Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։ + +--- + +Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap. + +--- + +Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu. +``` + +If we use a simple instructor prompt, even when we ask for the language to be correct, we oftentimes will get English instead. + +??? note "Expand to see documents examples" + + Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։ + + --- + + Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap. + + --- + + Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu. + + --- + + Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor. + + --- + + Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai. + + --- + + Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains. + + --- + + 近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。 + + --- + + In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte. + + --- + + पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है। + + --- + + 近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。 + + +In this example, we'll do something very simple, asking for the language to be correct. And generating a base model that only asks for a summary. To test we will use the library `langdetect` to detect the language of the text. To challenge us even more, we'll limit ourselves using 3.5 rather than 4 in order to use a 'dumber' model. + +```python +from pydantic import BaseModel, Field +from instructor import patch +from openai import AsyncOpenAI +from langdetect import detect + +docs = # To see the text, expand the notes above. + +# Patch the OpenAI client to enable response_model +client = patch(AsyncOpenAI()) + + +class GeneratedSummary(BaseModel): + summary: str + +async def summarize_text(text: str): + response = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=GeneratedSummary, + messages=[ + { + "role": "system", + "content": "Generate a concise summary in the language of the article. ", + }, + { + "role": "user", + "content": f"Summarize the following text in a concise way:\n{text}", + }, + ], + ) # type: ignore + return response.summary, text + + +if __name__ == "__main__": + import asyncio + + async def main(): + results = await asyncio.gather(*[summarize_text(doc) for doc in docs]) + for summary, doc in results: + source_lang = detect(doc) + target_lang = detect(summary) + print( + f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}" + ) + + asyncio.run(main()) + """ + Source: et, Summary: en, Match: False + Source: tl, Summary: tl, Match: True + Source: sw, Summary: en, Match: False + Source: tr, Summary: tr, Match: True + Source: vi, Summary: en, Match: False + Source: fr, Summary: fr, Match: True + Source: zh-cn, Summary: en, Match: False + Source: de, Summary: de, Match: True + Source: hi, Summary: en, Match: False + Source: ja, Summary: en, Match: False + """ +``` + +In this example, you'll notice that not all the languages are matching. Many of them respond in English, and so we get pretty terrible results. Only 3 out of 9 passed! + +## Reiterating instructions + +A simple trick that I found to work very well is to add a language detection attribute before the summary. + +```python hl_lines="2" +class GeneratedSummary(BaseModel): + detected_language: str = Field( + description="The language code of the original article. The summary must be generated in this same language.", + ) + summary: str +``` + +Just by adding this single attribute, we end up getting 100% correctness on language matches. If you want to see for yourself, checkout the complete script below + +```python +from pydantic import BaseModel, Field +from instructor import patch +from openai import AsyncOpenAI +from langdetect import detect + +docs = map( + lambda x: x.strip(), + """ +Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։ + +--- + +Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap. + +--- + +Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu. + +--- + +Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor. + +--- + +Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai. + +--- + +Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains. + +--- + +近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。 + +--- + +In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte. + +--- + +पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है। + +--- + +近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。 +""".split("---"), +) + +# Patch the OpenAI client to enable response_model +client = patch(AsyncOpenAI()) + + +class GeneratedSummary(BaseModel): + detected_language: str = Field( + description="The language code of the original article. The summary must be generated in this same language.", + ) + summary: str + +async def summarize_text(text: str): + response = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=GeneratedSummary, + messages=[ + { + "role": "system", + "content": "Generate a concise summary in the language of the article. ", + }, + { + "role": "user", + "content": f"Summarize the following text in a concise way:\n{text}", + }, + ], + ) # type: ignore + return response.summary, text + + +if __name__ == "__main__": + import asyncio + + async def main(): + results = await asyncio.gather(*[summarize_text(doc) for doc in docs]) + for summary, doc in results: + source_lang = detect(doc) + target_lang = detect(summary) + print( + f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}" + ) + + asyncio.run(main()) + """ + Source: et, Summary: et, Match: True + Source: tl, Summary: tl, Match: True + Source: sw, Summary: sw, Match: True + Source: tr, Summary: tr, Match: True + Source: vi, Summary: vi, Match: True + Source: fr, Summary: fr, Match: True + Source: zh-cn, Summary: zh-cn, Match: True + Source: de, Summary: de, Match: True + Source: hi, Summary: hi, Match: True + Source: ja, Summary: ja, Match: True + """ +``` diff --git a/docs/blog/posts/open_source.md b/docs/blog/posts/open_source.md new file mode 100644 index 000000000..9b3a766da --- /dev/null +++ b/docs/blog/posts/open_source.md @@ -0,0 +1,288 @@ +--- +draft: False +date: 2024-03-07 +slug: open-source-local-structured-output-pydantic-json-openai +tags: + - llms + - opensource + - together + - llama-cpp-python + - anyscale + - groq + - mistral + - ollama +authors: + - jxnl +--- + +# Structured Output for Open Source and Local LLMS + +Originally, Instructor facilitated API interactions solely via the OpenAI SDK, with an emphasis on function call by incorporating [Pydantic](https://pydantic-docs.helpmanual.io/) for structured data validation and serialization. + + +As the year progressed, we expanded our toolkit by integrating [JSON mode](../../concepts/patching.md#json-mode), thus enhancing our adaptability to vision models and open source models. This advancement now enables us to support an extensive range of models, from [GPT](https://openai.com/api/) and [Mistral](https://mistral.ai) to virtually any model accessible through [Ollama](https://ollama.ai) and [Hugging Face](https://huggingface.co/models), facilitated by [llama-cpp-python](../../hub/llama-cpp-python.md). For more insights into leveraging JSON mode with various models, refer back to our detailed guide on [Patching](../../concepts/patching.md). + +If you want to check out a course on how to use Instructor with Pydantic, check out our course on [Steering language models towards structured outputs.](https://www.wandb.courses/courses/steering-language-models). + + + + +## Exploring Different OpenAI Clients with Instructor + +The landscape of OpenAI clients is diverse, each offering unique functionalities tailored to different needs. Below, we explore some of the notable clients integrated with Instructor, providing structured outputs and enhanced capabilities, complete with examples of how to initialize and patch each client. + +## Local Models + +### Ollama: A New Frontier for Local Models + +Ollama's introduction significantly impacts the open-source community, offering a way to merge structured outputs with local models via JSON schema, as detailed in our [Ollama documentation](../../hub/ollama.md). + +For an in-depth exploration of Ollama, including setup and advanced features, refer to the documentation. The [Ollama official website](https://ollama.ai/download) also provides essential resources, model downloads, and community support for newcomers. + +``` +ollama run llama2 +``` + +```python +from openai import OpenAI +from pydantic import BaseModel +import instructor + + +class UserDetail(BaseModel): + name: str + age: int + + +# enables `response_model` in create call +client = instructor.from_openai( + OpenAI( + base_url="http://localhost:11434/v1", + api_key="ollama", # required, but unused + ), + mode=instructor.Mode.JSON, +) + +user = client.chat.completions.create( + model="llama2", + messages=[ + { + "role": "user", + "content": "Jason is 30 years old", + } + ], + response_model=UserDetail, +) + +print(user) +#> name='Jason' age=30 +``` + +### llama-cpp-python + +Open-source LLMS are gaining popularity, and llama-cpp-python has made the `llama-cpp` model available to obtain structured outputs using JSON schema via a mixture of [constrained sampling](https://llama-cpp-python.readthedocs.io/en/latest/#json-schema-mode) and [speculative decoding](https://llama-cpp-python.readthedocs.io/en/latest/#speculative-decoding). They also support a [OpenAI compatible client](https://llama-cpp-python.readthedocs.io/en/latest/#openai-compatible-web-server), which can be used to obtain structured output as an in-process mechanism to avoid any network dependency. + +For those interested in leveraging the power of llama-cpp-python for structured outputs, here's a quick example: + + +```python +import llama_cpp +import instructor + +from llama_cpp.llama_speculative import LlamaPromptLookupDecoding +from pydantic import BaseModel + + +llama = llama_cpp.Llama( + model_path="../../models/OpenHermes-2.5-Mistral-7B-GGUF/openhermes-2.5-mistral-7b.Q4_K_M.gguf", + n_gpu_layers=-1, + chat_format="chatml", + n_ctx=2048, + draft_model=LlamaPromptLookupDecoding(num_pred_tokens=2), + logits_all=True, + verbose=False, +) + + +create = instructor.patch( + create=llama.create_chat_completion_openai_v1, + mode=instructor.Mode.JSON_SCHEMA, +) + +class UserDetail(BaseModel): + name: str + age: int + + +user = create( + messages=[ + { + "role": "user", + "content": "Extract `Jason is 30 years old`", + } + ], + response_model=UserDetail, +) + +print(user) +#> name='Jason' age=30 +``` + +## Alternative Providers + +### Anyscale + +Anyscale's Mistral model, as detailed in our [Anyscale documentation](../../hub/anyscale.md) and on [Anyscale's official documentation](https://docs.anyscale.com/), introduces the ability to obtain structured outputs using JSON schema. + +```bash +export ANYSCALE_API_KEY="your-api-key" +``` + +```python +import os +from openai import OpenAI +from pydantic import BaseModel +import instructor + + +class UserDetails(BaseModel): + name: str + age: int + + +# enables `response_model` in create call +client = instructor.from_openai( + OpenAI( + base_url="https://api.endpoints.anyscale.com/v1", + api_key=os.environ["ANYSCALE_API_KEY"], + ), + # This uses Anyscale's json schema output mode + mode=instructor.Mode.JSON_SCHEMA, +) + +resp = client.chat.completions.create( + model="mistralai/Mixtral-8x7B-Instruct-v0.1", + messages=[ + {"role": "system", "content": "You are a world class extractor"}, + {"role": "user", "content": 'Extract the following entities: "Jason is 20"'}, + ], + response_model=UserDetails, +) +print(resp) +#> name='Jason' age=20 +``` + +### Groq + +Groq's platform, detailed further in our [Groq documentation](../../hub/groq.md) and on [Groq's official documentation](https://groq.com/), offers a unique approach to processing with its tensor architecture. This innovation significantly enhances the performance of structured output processing. + +```bash +export GROQ_API_KEY="your-api-key" +``` + +```python +import os +import instructor +import groq +from pydantic import BaseModel + +client = qrog.Groq( + api_key=os.environ.get("GROQ_API_KEY"), +) + +# By default, the patch function will patch the ChatCompletion.create and ChatCompletion.create methods to support the response_model parameter +client = instructor.from_openai(client, mode=instructor.Mode.MD_JSON) + + +# Now, we can use the response_model parameter using only a base model +# rather than having to use the OpenAISchema class +class UserExtract(BaseModel): + name: str + age: int + + +user: UserExtract = client.chat.completions.create( + model="mixtral-8x7b-32768", + response_model=UserExtract, + messages=[ + {"role": "user", "content": "Extract jason is 25 years old"}, + ], +) + +assert isinstance(user, UserExtract), "Should be instance of UserExtract" +print(user) +#> name='jason' age=25 +""" +``` + +### Together AI + +Together AI, when combined with Instructor, offers a seamless experience for developers looking to leverage structured outputs in their applications. For more details, refer to our [Together AI documentation](../hub/together.md) and explore the [patching guide](../concepts/patching.md) to enhance your applications. + +```bash +export TOGETHER_API_KEY="your-api-key" +``` + +```python +import os +import openai +from pydantic import BaseModel +import instructor + +client = openai.OpenAI( + base_url="https://api.together.xyz/v1", + api_key=os.environ["TOGETHER_API_KEY"], +) + +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) + +class UserExtract(BaseModel): + name: str + age: int + + +user: UserExtract = client.chat.completions.create( + model="mistralai/Mixtral-8x7B-Instruct-v0.1", + response_model=UserExtract, + messages=[ + {"role": "user", "content": "Extract jason is 25 years old"}, + ], +) + +assert isinstance(user, UserExtract), "Should be instance of UserExtract" +print(user) + +#> name='jason' age=25 +``` + +### Mistral + +For those interested in exploring the capabilities of Mistral Large with Instructor, we highly recommend checking out our comprehensive guide on [Mistral Large](../../hub/mistral.md). + +```python +import instructor + +from pydantic import BaseModel +from mistralai.client import MistralClient + +client = MistralClient() + +patched_chat = instructor.from_openai(create=client.chat, mode=instructor.Mode.MISTRAL_TOOLS) + +class UserDetails(BaseModel): + name: str + age: int + +resp = patched_chat( + model="mistral-large-latest", + response_model=UserDetails, + messages=[ + { + "role": "user", + "content": f'Extract the following entities: "Jason is 20"', + }, + ], +) +print(resp) +#> name='Jason' age=20 +``` diff --git a/docs/blog/posts/rag-and-beyond.md b/docs/blog/posts/rag-and-beyond.md index 985ab804d..b468c7b2b 100644 --- a/docs/blog/posts/rag-and-beyond.md +++ b/docs/blog/posts/rag-and-beyond.md @@ -98,7 +98,7 @@ import instructor from openai import OpenAI # Enables response_model in the openai client -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) query = client.chat.completions.create( model="gpt-4", @@ -179,7 +179,7 @@ import instructor from openai import OpenAI # Enables response_model in the openai client -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) retrieval = client.chat.completions.create( model="gpt-4", diff --git a/docs/blog/posts/validation-part1.md b/docs/blog/posts/validation-part1.md index 801a5c0bf..21f3cc863 100644 --- a/docs/blog/posts/validation-part1.md +++ b/docs/blog/posts/validation-part1.md @@ -39,7 +39,7 @@ from pydantic import BaseModel # This enables response_model keyword # from client.chat.completions.create -client = instructor.patch(OpenAI()) # (1)! +client = instructor.from_openai(OpenAI()) # (1)! class UserDetail(BaseModel): @@ -63,14 +63,14 @@ assert user.age == 25 1. To simplify your work with OpenAI models and streamline the extraction of Pydantic objects from prompts, we offer a patching mechanism for the `ChatCompletion` class. -2. Invalid responses that fail to be validated succesfully will trigger up to as many reattempts as you define. +2. Invalid responses that fail to be validated successfully will trigger up to as many reattempts as you define. 3. As long as you pass in a `response_model` parameter to the `ChatCompletion` api call, the returned object will always be a validated `Pydantic` object. In this post, we'll explore how to evolve from static, rule-based validation methods to dynamic, machine learning-driven ones. You'll learn to use `Pydantic` and `Instructor` to leverage language models and dive into advanced topics like content moderation, validating chain of thought reasoning, and contextual validation. -Let's examine how these approaches with a example. Imagine that you run a software company who wants to ensure you never serve hateful and racist content. This isn't an easy job since the language around these topics change very quickly and frequently. +Let's examine how these approaches with an example. Imagine that you run a software company that wants to ensure you never serve hateful and racist content. This isn't an easy job since the language around these topics change very quickly and frequently. ## Software 1.0: Introduction to Validations in Pydantic @@ -248,7 +248,7 @@ import instructor from openai import OpenAI # Enables `response_model` and `max_retries` parameters -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def validator(v): @@ -265,7 +265,7 @@ def validator(v): "content": f"Does `{v}` follow the rules: {statement}", }, ], - # this comes from client = instructor.patch(OpenAI()) + # this comes from client = instructor.from_openai(OpenAI()) response_model=Validation, # (1)! ) if not resp.is_valid: @@ -273,7 +273,7 @@ def validator(v): return v ``` -1. The new parameter of `response_model` comes from `client = instructor.patch(OpenAI())` and does not exist in the original OpenAI SDK. This +1. The new parameter of `response_model` comes from `client = instructor.from_openai(OpenAI())` and does not exist in the original OpenAI SDK. This allows us to pass in the `Pydantic` model that we want as a response. Now we can use this validator in the same way we used the `llm_validator` from `Instructor`. @@ -289,7 +289,7 @@ class UserMessage(BaseModel): A popular way of prompting large language models nowadays is known as chain of thought. This involves getting a model to generate reasons and explanations for an answer to a prompt. -We can utilise `Pydantic` and `Instructor` to perform a validation to check of the reasoning is reasonable, given both the answer and the chain of thought. To do this we can't build a field validator since we need to access multiple fields in the model. Instead we can use a model validator. +We can utilise `Pydantic` and `Instructor` to perform a validation to check if the reasoning is reasonable, given both the answer and the chain of thought. To do this we can't build a field validator since we need to access multiple fields in the model. Instead we can use a model validator. ```python def validate_chain_of_thought(values): @@ -307,7 +307,7 @@ def validate_chain_of_thought(values): "content": f"Verify that `{answer}` follows the chain of thought: {chain_of_thought}", }, ], - # this comes from client = instructor.patch(OpenAI()) + # this comes from client = instructor.from_openai(OpenAI()) response_model=Validation, ) if not resp.is_valid: @@ -402,16 +402,16 @@ Value error, Citation `Jason is cool` not found in text chunks [type=value_error For further information visit https://errors.pydantic.dev/2.4/v/value_error ``` -## Putting it all together with `client = instructor.patch(OpenAI())` +## Putting it all together with `client = instructor.from_openai(OpenAI())` -To pass this context from the `client.chat.completions.create` call, `client = instructor.patch(OpenAI())` also passes the `validation_context`, which will be accessible from the `info` argument in the decorated validator functions. +To pass this context from the `client.chat.completions.create` call, `client = instructor.from_openai(OpenAI())` also passes the `validation_context`, which will be accessible from the `info` argument in the decorated validator functions. ```python from openai import OpenAI import instructor # Enables `response_model` and `max_retries` parameters -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def answer_question(question: str, text_chunk: str) -> AnswerWithCitation: @@ -430,7 +430,7 @@ def answer_question(question: str, text_chunk: str) -> AnswerWithCitation: ## Error Handling and Re-Asking -Validators can ensure certain properties of the outputs by throwing errors, in an AI system we can use the errors and allow language model to self correct. The by running `client = instructor.patch(OpenAI())` not only do we add `response_model` and `validation_context` it also allows you to use the `max_retries` parameter to specify the number of times to try and self correct. +Validators can ensure certain properties of the outputs by throwing errors, in an AI system we can use the errors and allow language model to self correct. Then by running `client = instructor.from_openai(OpenAI())` not only do we add `response_model` and `validation_context` it also allows you to use the `max_retries` parameter to specify the number of times to try and self correct. This approach provides a layer of defense against two types of bad outputs: @@ -465,7 +465,7 @@ model = client.chat.completions.create( messages=[ {"role": "user", "content": "Extract jason is 25 years old"}, ], - # Powered by client = instructor.patch(OpenAI()) + # Powered by client = instructor.from_openai(OpenAI()) response_model=UserModel, max_retries=2, ) @@ -477,6 +477,6 @@ In this example, even though there is no code explicitly transforming the name t ## Conclusion -From the simplicity of Pydantic and Instructor to the dynamic validation capabilities of LLMs, the landscape of validation is changing but without needing to introduce new contepts. It's clear that the future of validation is not just about preventing bad data but about allowing llms to understand the data and correcting it. +From the simplicity of Pydantic and Instructor to the dynamic validation capabilities of LLMs, the landscape of validation is changing but without needing to introduce new concepts. It's clear that the future of validation is not just about preventing bad data but about allowing llms to understand the data and correcting it. If you enjoy the content or want to try out `Instructor` please check out the [github](https://github.com/jxnl/instructor) and give us a star! diff --git a/docs/blog/posts/version-1.md b/docs/blog/posts/version-1.md new file mode 100644 index 000000000..69f4b692b --- /dev/null +++ b/docs/blog/posts/version-1.md @@ -0,0 +1,257 @@ +--- +draft: False +date: 2024-04-01 +slug: announce-instructor-v1 +tags: + - instructor +authors: + - jxnl +--- + +# Announcing instructor=1.0.0 + +Over the past 10 months, we've build up instructor with the [principle](../../why.md) of 'easy to try, and easy to delete'. We accomplished this by patching the openai client with the `instructor` package and adding new arguments like `response_model`, `max_retries`, and `validation_context`. As a result I truely believe isntructor is the [best way](./best_framework.md) to get structured data out of llm apis. + +But as a result, we've been a bit stuck on getting typing to work well while giving you more control at development time. I'm excited to launch version 1.0.0 which cleans up the api w.r.t. typing without compromising the ease of use. + + + +## Growth + +Over the past 10 months, we've enjoyed healthy growth with over 4000+ github stars and 100+ contributors, and more importantly, 120k monthly downloads, and 20k unique monthly visitors with 500k requests per month to our docs + +![downloads](./img/downloads.png) + +## Whats new? + +Honestly, nothing much, the simplest change you'll need to make is to replace `instructor.patch` with `instructor.from_openai`. + +```python +import openai +import instructor + +client = instructor.from_openai(openai.OpenAI()) +``` + +Except now, any default arguments you want to place into the `create` call will be passed to the client. via kwargs. + +IF you know you want to pass in tempurature, seed, or model, you can do so. + +```python + +import openai +import instructor + +client = instructor.from_openai( + openai.OpenAI(), + model="gpt-4-turbo-preview", + temperature=0.2 +) +``` + +Now, whenever you call `client.chat.completions.create` the `model` and `temperature` will be passed to the openai client! + +## No new Standards + +When I first started working on this project, my goal was to ensure that we weren't introducing any new standards. Instead, our focus was on maintaining compatibility with existing ones. By creating our own client, we can seamlessly proxy OpenAI's `chat.completions.create` and Anthropic's `messages.create` methods. This approach allows us to provide a smooth upgrade path for your client, enabling support for all the latest models and features as they become available. Additionally, this strategy safeguards us against potential downstream changes. + +```python +import openai +import anthropic +import litellm +import instructor + +# These are all ways to create a client +client = instructor.from_openai(openai.OpenAI()) +client = instructor.from_anthropic(anthropic.Anthropic()) +client = instructor.from_litellm(litellm.completion) + +# all of these will route to the same underlying create function +# allow you to add instructor to try it out, while easily removing it +client.create(..., response_model=Type[T]) -> T +client.chat.completions.create(..., response_model=Type[T]) -> T +client.messages.create(..., response_model=Type[T]) -> T +``` + +## Type are infered correctly + +This was the dream of instructor but due to the patching of openai, it wasnt possible for me to get typing to work well. Now, with the new client, we can get typing to work well! We've also added a few `create_*` methods to make it easier to create iterables and partials, and to access the original completion. + +### Calling `create` + +```python +import openai +import instructor +from pydantic import BaseModel + +class User(BaseModel): + name: str + age: int + +client = instructor.from_openai(openai.OpenAI()) + +user = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) +``` + +Now if you use a ID, you can see the type is correctly infered. + +![type](./img/type.png) + +### Handling async: `await create` + +This will also work correctly with asynchronous clients. + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.AsyncOpenAI()) + + +class User(BaseModel): + name: str + age: int + + +async def extract(): + return await client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, + ) +``` + +Notice that simply because we return the `create` method, the `extract()` function will return the correct user type. + +![async](./img/async_type.png) + +### Returning the original completion: `create_with_completion` + +You can also return the original completion object + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +user, completion = client.chat.completions.create_with_completion( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) +``` + +![with_completion](./img/with_completion.png) + + +### Streaming Partial Objects: `create_partial` + +In order to handle streams, we still support `Iterable[T]` and `Partial[T]` but to simply the type inference, we've added `create_iterable` and `create_partial` methods as well! + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +user_stream = client.chat.completions.create_partial( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) + +for user in user_stream: + print(user) + # name=None age=None + # name='' age=None + # name='John' age=None + # name='John Doe' age=None + # name='John Doe' age=30 +``` + +Notice now that the type infered is `Generator[User, None]` + +![generator](./img/generator.png) + +### Streaming Iterables: `create_iterable` + +We get an iterable of objects when we want to extract multiple objects. + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +users = client.chat.completions.create_iterable( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create 2 users"}, + ], + response_model=User, +) + +for user in users: + print(user) + # User(name='John Doe', age=30) + # User(name='Jane Smith', age=25) +``` + +![iterable](./img/iterable.png) + +## Validation and Error Handling + +Instructor has always supported validation and error handling. But now, we've added a new `validation_context` argument to the `create` call. This allows you to pass in a `ValidationContext` object which will be passed to the `response_model`. This allows you to add custom validation logic to the `response_model`. + +If you want to learn more check out the docs on [retrying](../../concepts/retrying.md) and [reasking](../../concepts/reask_validation.md) + +## Support in multiple languages + +While each flavor is different the core philosophy is the same. Keeping it as close as possible to the common api allows us to support all the same features in all the same languages by hooking into each libraries's popular validation libraries. + +Check out: + +- [JavaScript](https://github.com/instructor-ai/instructor-js) +- [Elixir](https://github.com/instructor-ai/instructor-elixir) +- [PHP](https://github.com/cognesy/instructor-php) + +If you're interested in contributing, check out the [contributing guide](../../contributing.md), and you want to create instructor in your language, let [me](https://twitter.com/jxnlco) know and I can help with promotion and connecting all the docs! \ No newline at end of file diff --git a/docs/concepts/caching.md b/docs/concepts/caching.md index 25b519cc0..6055e42f8 100644 --- a/docs/concepts/caching.md +++ b/docs/concepts/caching.md @@ -11,7 +11,7 @@ import openai import instructor from pydantic import BaseModel -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class UserDetail(BaseModel): @@ -33,12 +33,12 @@ def extract(data) -> UserDetail: start = time.perf_counter() # (1) model = extract("Extract jason is 25 years old") print(f"Time taken: {time.perf_counter() - start}") -#> Time taken: 0.8392175831831992 +#> Time taken: 0.5761224159505218 start = time.perf_counter() model = extract("Extract jason is 25 years old") # (2) print(f"Time taken: {time.perf_counter() - start}") -#> Time taken: 8.33999365568161e-07 +#> Time taken: 4.37488779425621e-06 ``` 1. Using `time.perf_counter()` to measure the time taken to run the function is better than using `time.time()` because it's more accurate and less susceptible to system clock changes. @@ -139,7 +139,7 @@ import diskcache from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) cache = diskcache.Cache('./my_cache_directory') @@ -243,7 +243,7 @@ import instructor from pydantic import BaseModel from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) cache = redis.Redis("localhost") diff --git a/docs/concepts/enums.md b/docs/concepts/enums.md index 872f1cb85..9e96e55d8 100644 --- a/docs/concepts/enums.md +++ b/docs/concepts/enums.md @@ -20,7 +20,7 @@ class UserDetail(BaseModel): ) ``` -If you're having a hard time with `Enum` and alternative is to use `Literal` instead. +If you're having a hard time with `Enum` an alternative is to use `Literal` instead. ```python hl_lines="4" from typing import Literal diff --git a/docs/concepts/fastapi.md b/docs/concepts/fastapi.md index 99c5febc9..855a5d273 100644 --- a/docs/concepts/fastapi.md +++ b/docs/concepts/fastapi.md @@ -20,7 +20,7 @@ from pydantic import BaseModel from openai import AsyncOpenAI # Enables response_model -client = instructor.patch(AsyncOpenAI()) +client = instructor.from_openai(AsyncOpenAI()) app = FastAPI() @@ -81,7 +81,7 @@ async def extract(data: UserData): ) async def generate(): - for user in users: + async for user in users: resp_json = user.model_dump_json() yield f"data: {resp_json}" yield "data: [DONE]" diff --git a/docs/concepts/fields.md b/docs/concepts/fields.md index d1bfdf425..69fae4b8e 100644 --- a/docs/concepts/fields.md +++ b/docs/concepts/fields.md @@ -86,6 +86,26 @@ print(date_range.model_dump_json()) #> {"start_date":"2021-01-01","end_date":"2021-01-30"} ``` +## Omitting fields from schema sent to the language model + +In some cases, you may wish to have the language model ignore certain fields in your model. You can do this by using Pydantic's `SkipJsonSchema` annotation. This omits a field from the JSON schema emitted by Pydantic (which `instructor` uses for constructing its prompts and tool definitions). For example: + +```py +from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema + + +class Response(BaseModel): + question: str + answer: str + private_field: SkipJsonSchema[str | None] = None + + +assert "private_field" not in Response.model_json_schema()["properties"] +``` + +Note that because the language model will never return a value for `private_field`, you'll need a default value (this can be a generator via a declared Pydantic `Field`). + ## Customizing JSON Schema There are some fields that are exclusively used to customise the generated JSON Schema: diff --git a/docs/concepts/lists.md b/docs/concepts/lists.md index ad98ce7f8..2a7a09346 100644 --- a/docs/concepts/lists.md +++ b/docs/concepts/lists.md @@ -55,7 +55,7 @@ from openai import OpenAI from typing import Iterable from pydantic import BaseModel -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.function_calls.Mode.JSON) class User(BaseModel): @@ -95,7 +95,7 @@ import openai from typing import Iterable from pydantic import BaseModel -client = instructor.patch(openai.OpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS) class User(BaseModel): @@ -137,7 +137,7 @@ import openai from typing import Iterable from pydantic import BaseModel -client = instructor.patch(openai.AsyncOpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.AsyncOpenAI(), mode=instructor.Mode.TOOLS) class UserExtract(BaseModel): @@ -157,8 +157,8 @@ async def print_iterable_results(): ) async for m in model: print(m) - #> name='John Smith' age=30 - #> name='Mary Jane' age=28 + #> name='John Doe' age=32 + #> name='Jane Doe' age=28 import asyncio diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md index f06a1c438..77e4c0fc9 100644 --- a/docs/concepts/logging.md +++ b/docs/concepts/logging.md @@ -1,4 +1,4 @@ -In order to see the requests made to OpenAI and the responses, you can set logging to DEBUG. This will show the requests and responses made to OpenAI. This can be useful for debugging and understanding the requests and responses made to OpenAI. +In order to see the requests made to OpenAI and the responses, you can set logging to DEBUG. This will show the requests and responses made to OpenAI. This can be useful for debugging and understanding the requests and responses made to OpenAI. I would love some contributions that make this a lot cleaner, but for now this is the fastest way to see the prompts. ```python import instructor @@ -7,10 +7,11 @@ import logging from pydantic import BaseModel + # Set logging to DEBUG logging.basicConfig(level=logging.DEBUG) -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class UserDetail(BaseModel): @@ -24,5 +25,16 @@ user = client.chat.completions.create( messages=[ {"role": "user", "content": "Extract Jason is 25 years old"}, ], -) -``` +) # type: ignore + +""" +... +DEBUG:instructor:Patching `client.chat.completions.create` with mode= +DEBUG:instructor:Instructor Request: mode.value='tool_call', response_model=, new_kwargs={'model': 'gpt-3.5-turbo', 'messages': [{'role': 'user', 'content': 'Extract Jason is 25 years old'}], 'tools': [{'type': 'function', 'function': {'name': 'UserDetail', 'description': 'Correctly extracted `UserDetail` with all the required parameters with correct types', 'parameters': {'properties': {'name': {'title': 'Name', 'type': 'string'}, 'age': {'title': 'Age', 'type': 'integer'}}, 'required': ['age', 'name'], 'type': 'object'}}}], 'tool_choice': {'type': 'function', 'function': {'name': 'UserDetail'}}} +DEBUG:instructor:max_retries: 1 +... +DEBUG:instructor:Instructor Pre-Response: ChatCompletion(id='chatcmpl-8zBxMxsOqm5Sj6yeEI38PnU2r6ncC', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_E1cftF5U0zEjzIbWt3q0ZLbN', function=Function(arguments='{"name":"Jason","age":25}', name='UserDetail'), type='function')]))], created=1709594660, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint='fp_2b778c6b35', usage=CompletionUsage(completion_tokens=9, prompt_tokens=81, total_tokens=90)) +DEBUG:httpcore.connection:close.started +DEBUG:httpcore.connection:close.complete +""" +``` \ No newline at end of file diff --git a/docs/concepts/maybe.md b/docs/concepts/maybe.md index 183340b1d..dc6c70a93 100644 --- a/docs/concepts/maybe.md +++ b/docs/concepts/maybe.md @@ -41,7 +41,7 @@ from pydantic import BaseModel, Field from typing import Optional # This enables the `response_model` keyword -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class UserDetail(BaseModel): diff --git a/docs/concepts/models.md b/docs/concepts/models.md index 7a6d1e461..c06361148 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -30,7 +30,7 @@ Here all docstrings, types, and field annotations will be used to generate the p ## Optional Values -If we use `Optional` and `default`, they will be considered not required when sent to the language model +If we use `Optional` and `default`, they will be considered not required when sent to the language model. ```python from pydantic import BaseModel, Field @@ -43,6 +43,8 @@ class User(BaseModel): email: Optional[str] = Field(description="The email of the user.", default=None) ``` +Note that fields can also be omitted entirely from being sent to the language model by using Pydantic's `SkipJsonSchema` annotation. See [Fields](fields.md#omitting-fields-from-schema-sent-to-the-language-model) for additional details. + ## Dynamic model creation There are some occasions where it is desirable to create a model using runtime information to specify the fields. For this, Pydantic provides the create_model function to allow models to be created on the fly: @@ -141,7 +143,7 @@ from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class SearchQuery(BaseModel): @@ -150,7 +152,7 @@ class SearchQuery(BaseModel): def execute(self): print(f"Searching for {self.query} of type {self.query_type}") - #> Searching for cat pictures of type image + #> Searching for cat of type image return "Results for cat" diff --git a/docs/concepts/parallel.md b/docs/concepts/parallel.md index 2152533e0..22b4d0123 100644 --- a/docs/concepts/parallel.md +++ b/docs/concepts/parallel.md @@ -28,7 +28,9 @@ class GoogleSearch(BaseModel): query: str -client = instructor.patch(openai.OpenAI(), mode=instructor.Mode.PARALLEL_TOOLS) # (1)! +client = instructor.from_openai( + openai.OpenAI(), mode=instructor.Mode.PARALLEL_TOOLS +) # (1)! function_calls = client.chat.completions.create( model="gpt-4-turbo-preview", @@ -44,9 +46,9 @@ function_calls = client.chat.completions.create( for fc in function_calls: print(fc) - #> location='Toronto' units='imperial' + #> location='Toronto' units='metric' #> location='Dallas' units='imperial' - #> query='who won the super bowl' + #> query='super bowl winner' ``` 1. Set the mode to `PARALLEL_TOOLS` to enable parallel function calling. diff --git a/docs/concepts/partial.md b/docs/concepts/partial.md index 7a543e750..e3151640b 100644 --- a/docs/concepts/partial.md +++ b/docs/concepts/partial.md @@ -2,7 +2,7 @@ Field level streaming provides incremental snapshots of the current state of the response model that are immediately useable. This approach is particularly relevant in contexts like rendering UI components. -Instructor supports this pattern by making use of `Partial[T]`. This lets us dynamically create a new class that treats all of the original model's fields as `Optional`. +Instructor supports this pattern by making use of `create_partial`. This lets us dynamically create a new class that treats all of the original model's fields as `Optional`. ## Understanding Partial Responses @@ -26,7 +26,7 @@ If we streamed json out from OpenAI, we would only be able to parse when the obj {"name": "John", "age": 25} # Completed ``` -When specifying a `Partial[T]` and setting `stream=True`, the response from `instructor` becomes a `Generator[T]`. As the generator yields results, you can iterate over these incremental updates. The last value yielded by the generator represents the completed extraction! +When specifying a `create_partial` and setting `stream=True`, the response from `instructor` becomes a `Generator[T]`. As the generator yields results, you can iterate over these incremental updates. The last value yielded by the generator represents the completed extraction! ``` {"name": "Jo => User(name="Jo", age=None) @@ -37,7 +37,7 @@ When specifying a `Partial[T]` and setting `stream=True`, the response from `ins !!! warning "Limited Validator Support" - Fewer validators are supported by `Partial` response models as streamed fields will natural raise validation error, as we do not have a strong opinoin on how to handle them. + Due to the streaming nature of the response model, we do not support validators since they would not be able to be applied to the streaming response. Let's look at an example of streaming an extraction of conference information, that would be used to stream in an react component. @@ -48,7 +48,7 @@ from pydantic import BaseModel from typing import List from rich.console import Console -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) text_block = """ In our recent online meeting, participants from various backgrounds joined to discuss the upcoming tech conference. The names and contact details of the participants were as follows: @@ -79,9 +79,9 @@ class MeetingInfo(BaseModel): deadline: str -extraction_stream = client.chat.completions.create( +extraction_stream = client.chat.completions.create_partial( model="gpt-4", - response_model=instructor.Partial[MeetingInfo], + response_model=MeetingInfo, messages=[ { "role": "user", @@ -119,10 +119,10 @@ print(extraction.model_dump_json(indent=2)) "twitter": "@CodeMaster2023" } ], - "date": "2024-03-15", + "date": "March 15th, 2024", "location": "Grand Tech Arena located at 4521 Innovation Drive", "budget": 50000, - "deadline": "2024-02-20" + "deadline": "February 20th" } """ ``` @@ -140,7 +140,7 @@ import instructor from openai import AsyncOpenAI from pydantic import BaseModel -client = instructor.patch(AsyncOpenAI()) +client = instructor.from_openai(AsyncOpenAI()) class User(BaseModel): @@ -149,9 +149,9 @@ class User(BaseModel): async def print_partial_results(): - user = await client.chat.completions.create( + user = client.chat.completions.create_partial( model="gpt-4-turbo-preview", - response_model=instructor.Partial[User], + response_model=User, max_retries=2, stream=True, messages=[ @@ -161,8 +161,14 @@ async def print_partial_results(): async for m in user: print(m) #> name=None age=None - #> name='' age=None + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=None #> name='Jason' age=None + #> name='Jason' age=None + #> name='Jason' age=None + #> name='Jason' age=12 #> name='Jason' age=12 diff --git a/docs/concepts/patching.md b/docs/concepts/patching.md index 1ebe3eaf7..cc89dcce3 100644 --- a/docs/concepts/patching.md +++ b/docs/concepts/patching.md @@ -16,7 +16,7 @@ This is the recommended method for OpenAI clients. It is the most stable as func import instructor from openai import OpenAI -client = instructor.patch(OpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.TOOLS) ``` ## Parallel Tool Calling @@ -27,7 +27,7 @@ Parallel tool calling is also an option but you must set `response_model` to be import instructor from openai import OpenAI -client = instructor.patch(OpenAI(), mode=instructor.Mode.PARALLEL_TOOLS) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.PARALLEL_TOOLS) ``` ## Function Calling @@ -38,7 +38,7 @@ Note that function calling is soon to be deprecated in favor of TOOL mode for Op import instructor from openai import OpenAI -client = instructor.patch(OpenAI(), mode=instructor.Mode.FUNCTIONS) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.FUNCTIONS) ``` ## JSON Mode @@ -49,18 +49,7 @@ JSON mode uses OpenAI's JSON fromat for responses. by setting `response_format={ import instructor from openai import OpenAI -client = instructor.patch(OpenAI(), mode=instructor.Mode.JSON) -``` - -## JSON Schema Mode - -JSON Schema mode uses OpenAI's JSON fromat for responses. by setting `response_format={"type": "json_object", schema:response_model.model_json_schema()}` in the `chat.completions.create` method. This is only available for select clients (e.g. llama-cpp-python, Anyscale, Together) - -```python -import instructor -from openai import OpenAI - -client = instructor.patch(OpenAI(), mode=instructor.Mode.JSON_SCHEMA) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.JSON) ``` ## Markdown JSON Mode @@ -75,5 +64,5 @@ This just asks for the response in JSON format, but it is not recommended, and m import instructor from openai import OpenAI -client = instructor.patch(OpenAI(), mode=instructor.Mode.MD_JSON) -``` +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON) +``` \ No newline at end of file diff --git a/docs/concepts/prompting.md b/docs/concepts/prompting.md index 46a8546ea..667781e14 100644 --- a/docs/concepts/prompting.md +++ b/docs/concepts/prompting.md @@ -115,7 +115,7 @@ class UserDetail(BaseModel): ) ``` -If you're having a hard time with `Enum` and alternative is to use `Literal` +If you're having a hard time with `Enum` an alternative is to use `Literal` ```python hl_lines="4" from typing import Literal diff --git a/docs/concepts/raw_response.md b/docs/concepts/raw_response.md index 7248282c0..8d58a6a07 100644 --- a/docs/concepts/raw_response.md +++ b/docs/concepts/raw_response.md @@ -1,4 +1,7 @@ -Often times not only do you want the base model but may also want the original response from the API. You can do this by retrieving the `raw_response`, since the `raw_response` is also a pydantic model, you can use any of the pydantic model methods on it. + +# Creating a model with completions + +In instructor>1.0.0 we have a custom client, if you wish to use the raw response you can do the following ```python import instructor @@ -6,7 +9,7 @@ import instructor from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserExtract(BaseModel): @@ -14,7 +17,7 @@ class UserExtract(BaseModel): age: int -user: UserExtract = client.chat.completions.create( +user, completion = client.chat.completions.create_with_completion( model="gpt-3.5-turbo", response_model=UserExtract, messages=[ @@ -22,10 +25,13 @@ user: UserExtract = client.chat.completions.create( ], ) -print(user._raw_response) +print(user) +#> name='Jason' age=25 + +print(completion) """ ChatCompletion( - id='chatcmpl-8u9bsrmmf5YjZyfCtQymoZV8LK1qg', + id='chatcmpl-9AUijAMiPb1gQ3qs2bdJepRTVlWip', choices=[ Choice( finish_reason='stop', @@ -37,7 +43,7 @@ ChatCompletion( function_call=None, tool_calls=[ ChatCompletionMessageToolCall( - id='call_O5rpXf47YgXiYrYWv45yZUeM', + id='call_yAwmIEIXBdMaKvU8aXnsjdem', function=Function( arguments='{"name":"Jason","age":25}', name='UserExtract' ), @@ -47,17 +53,11 @@ ChatCompletion( ), ) ], - created=1708394000, + created=1712288397, model='gpt-3.5-turbo-0125', object='chat.completion', - system_fingerprint='fp_69829325d0', + system_fingerprint='fp_b28b39ffa8', usage=CompletionUsage(completion_tokens=9, prompt_tokens=82, total_tokens=91), ) """ -``` - -!!! tip "Accessing tokens usage" - - This is the recommended way to access the tokens usage, since it is a pydantic model you can use any of the pydantic model methods on it. For example, you can access the `total_tokens` by doing `user._raw_response.usage.total_tokens`. Note that this also includes the tokens used during any previous unsuccessful attempts. - - In the future, we may add additional hooks to the `raw_response` to make it easier to access the tokens usage. +``` \ No newline at end of file diff --git a/docs/concepts/reask_validation.md b/docs/concepts/reask_validation.md index dc21efe49..f0cffa9bd 100644 --- a/docs/concepts/reask_validation.md +++ b/docs/concepts/reask_validation.md @@ -41,7 +41,7 @@ except ValidationError as e: 1 validation error for UserDetail name Value error, Name must contain a space. [type=value_error, input_value='Jason', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/value_error + For further information visit https://errors.pydantic.dev/2.7/v/value_error """ ``` @@ -61,23 +61,21 @@ LLM-based validation can also be plugged into the same Pydantic model. Here, if ```python hl_lines="9 15" import instructor - from openai import OpenAI from instructor import llm_validator from pydantic import BaseModel, ValidationError, BeforeValidator from typing_extensions import Annotated + # Apply the patch to the OpenAI client -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class QuestionAnswer(BaseModel): question: str answer: Annotated[ str, - BeforeValidator( - llm_validator("don't say objectionable things", openai_client=client) - ), + BeforeValidator(llm_validator("don't say objectionable things", client=client)), ] @@ -91,8 +89,8 @@ except ValidationError as e: """ 1 validation error for QuestionAnswer answer - Assertion failed, The statement promotes objectionable behavior by encouraging evil and theft. [type=assertion_error, input_value='The meaning of life is to be evil and steal', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/assertion_error + Assertion failed, The statement promotes objectionable behavior by encouraging evil actions like stealing, which goes against the rule of not saying objectionable things. [type=assertion_error, input_value='The meaning of life is to be evil and steal', input_type=str] + For further information visit https://errors.pydantic.dev/2.7/v/assertion_error """ ``` @@ -125,7 +123,7 @@ import instructor from pydantic import BaseModel, field_validator # Apply the patch to the OpenAI client -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class UserDetails(BaseModel): @@ -149,7 +147,7 @@ import instructor import openai from pydantic import BaseModel -client = instructor.patch(openai.OpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS) class UserDetails(BaseModel): @@ -177,7 +175,7 @@ print(model.model_dump_json(indent=2)) ### What happens behind the scenes? -Behind the scenes, the `instructor.patch()` method adds a `max_retries` parameter to the `openai.ChatCompletion.create()` method. The `max_retries` parameter will trigger up to 2 reattempts if the `name` attribute fails the uppercase validation in `UserDetails`. +Behind the scenes, the `instructor.from_openai()` method adds a `max_retries` parameter to the `openai.ChatCompletion.create()` method. The `max_retries` parameter will trigger up to 2 reattempts if the `name` attribute fails the uppercase validation in `UserDetails`. ```python from pydantic import ValidationError diff --git a/docs/concepts/retrying.md b/docs/concepts/retrying.md index 831ba267d..800b63db0 100644 --- a/docs/concepts/retrying.md +++ b/docs/concepts/retrying.md @@ -28,13 +28,13 @@ class UserDetail(BaseModel): try: UserDetail(name="jason", age=12) -except Exception as e: +except InstructorRetryException as e: print(e) """ 1 validation error for UserDetail name Value error, Name must be ALL CAPS [type=value_error, input_value='jason', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/value_error + For further information visit https://errors.pydantic.dev/2.7/v/value_error """ ``` @@ -53,7 +53,7 @@ class UserDetail(BaseModel): age: int -client = instructor.patch(openai.OpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS) response = client.chat.completions.create( model="gpt-4-turbo-preview", @@ -76,6 +76,56 @@ print(response.model_dump_json(indent=2)) 1. We set the maximum number of retries to 3. This means that if the model returns an error, we'll reask the model up to 3 times. 2. We assert that the name is in all caps. +## Catching Retry Exceptions + +If you want to catch the retry exceptions, you can do so and access the `last_completion`, `n_attempts` and `messages` attributes. + +```python +from openai import OpenAI +from instructor import from_openai +from instructor.retry import InstructorRetryException +from pydantic import BaseModel, field_validator + +# Patch the OpenAI client to enable response_model +client = from_openai(OpenAI()) + + +# Define a Pydantic model for the user details +class UserDetail(BaseModel): + name: str + age: int + + @field_validator("age") + def validate_age(cls, v): + raise ValueError("You will never succeed") + + +# Use the client to create a user detail +try: + user: UserDetail = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[{"role": "user", "content": "Extract Jason is 25 years old"}], + max_retries=3, + ) +except InstructorRetryException as e: + print(e) + """ + 1 validation error for UserDetail + age + Value error, You will never succeed [type=value_error, input_value=25, input_type=int] + For further information visit https://errors.pydantic.dev/2.7/v/value_error + """ + + print(e.n_attempts) + #> 3 + + print(e.last_completion) + """ + ChatCompletion(id='chatcmpl-9FaHq4dL4SszLAbErGlpD3a0TYxi0', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_XidgLpIu1yfaq876L65k91RM', function=Function(arguments='{"name":"Jason","age":25}', name='UserDetail'), type='function')]))], created=1713501434, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint='fp_d9767fc5b9', usage=CompletionUsage(completion_tokens=27, prompt_tokens=513, total_tokens=540)) + """ +``` + ## Advanced: Retry Logic If you want more control over how we define retries such as back-offs and additional retry logic we can use a library called Tenacity. To learn more, check out the documentation on the [Tenacity](https://tenacity.readthedocs.io/en/latest/) website. @@ -88,7 +138,7 @@ import instructor from pydantic import BaseModel from tenacity import Retrying, stop_after_attempt, wait_fixed -client = instructor.patch(openai.OpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS) class UserDetail(BaseModel): @@ -130,7 +180,7 @@ import instructor from pydantic import BaseModel from tenacity import AsyncRetrying, stop_after_attempt, wait_fixed -client = instructor.patch(openai.AsyncOpenAI(), mode=instructor.Mode.TOOLS) +client = instructor.from_openai(openai.AsyncOpenAI(), mode=instructor.Mode.TOOLS) class UserDetail(BaseModel): @@ -175,3 +225,67 @@ Tenacity features a huge number of different retrying capabilities. A few of the - `Retrying(wait=(wait_fixed(1) + wait_random(0.2)))`: Wait at least 1 second and add up to 0.2 seconds Remember that for async clients you need to use `AsyncRetrying` instead of `Retrying`! + + +## Retry Callbacks + +You can also define callbacks to be called before and after each attempt. This is useful for logging or debugging. + +```python +from pydantic import BaseModel, field_validator +from openai import OpenAI +import instructor +import tenacity + +client = OpenAI() +client = instructor.from_openai(client) + + +class User(BaseModel): + name: str + age: int + + @field_validator("name") + def name_is_uppercase(cls, v: str): + assert v.isupper(), "Name must be uppercase" + return v + + +resp = client.messages.create( + model="gpt-3.5-turbo", + max_tokens=1024, + max_retries=tenacity.Retrying( + stop=tenacity.stop_after_attempt(3), + before=lambda _: print("before:", _), +""" +before: + +""" + after=lambda _: print("after:", _), + ), + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old.", + } + ], + response_model=User, +) # type: ignore + +assert isinstance(resp, User) +assert resp.name == "JOHN" # due to validation +assert resp.age == 18 +print(resp) +#> name='JOHN' age=18 + +""" +before: +after: + +before: +name='JOHN' age=18 +""" +``` diff --git a/docs/concepts/types.md b/docs/concepts/types.md index cd62fadce..32c0863ad 100644 --- a/docs/concepts/types.md +++ b/docs/concepts/types.md @@ -28,6 +28,7 @@ print(model.model_json_schema()) 'type': 'boolean', } }, + 'required': ['content'], 'title': 'Response', 'type': 'object', } @@ -40,7 +41,7 @@ print(model.model_json_schema()) import instructor import openai -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) # Response model with simple types like str, int, float, bool resp = client.chat.completions.create( @@ -68,7 +69,7 @@ import openai from typing import Annotated from pydantic import Field -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) UpperCaseStr = Annotated[str, Field(description="string must be upper case")] @@ -97,7 +98,7 @@ import instructor import openai from typing import Literal -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) resp = client.chat.completions.create( model="gpt-3.5-turbo", @@ -129,7 +130,7 @@ class Label(str, Enum): SHIPPING = "SHIPPING" -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) resp = client.chat.completions.create( model="gpt-3.5-turbo", @@ -153,7 +154,7 @@ import instructor import openai from typing import List -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) resp = client.chat.completions.create( model="gpt-3.5-turbo", @@ -181,7 +182,7 @@ import openai from pydantic import BaseModel from typing import Union -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class Add(BaseModel): @@ -260,7 +261,7 @@ MarkdownDataFrame = Annotated[ ] -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) resp = client.chat.completions.create( model="gpt-3.5-turbo", @@ -294,7 +295,7 @@ import openai from pydantic import BaseModel from typing import Union, List -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class Weather(BaseModel, frozen=True): diff --git a/docs/concepts/usage.md b/docs/concepts/usage.md index 6eed3602c..530c13763 100644 --- a/docs/concepts/usage.md +++ b/docs/concepts/usage.md @@ -6,7 +6,7 @@ import instructor from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserExtract(BaseModel): @@ -14,7 +14,7 @@ class UserExtract(BaseModel): age: int -user: UserExtract = client.chat.completions.create( +user, completion = client.chat.completions.create_with_completion( model="gpt-3.5-turbo", response_model=UserExtract, messages=[ @@ -22,6 +22,6 @@ user: UserExtract = client.chat.completions.create( ], ) -print(user._raw_response.usage) +print(completion.usage) #> CompletionUsage(completion_tokens=9, prompt_tokens=82, total_tokens=91) ``` diff --git a/docs/contributing.md b/docs/contributing.md index 90c1f08da..952f3af79 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,8 +1,8 @@ We would love for you to contribute to `Instructor`. -## [Evals](https://github.com/jxnl/instructor/tree/main/tests/openai/evals) +## [Evals](https://github.com/jxnl/instructor/tree/main/tests/llm/test_openai/evals) -We invite you to contribute evals in pytest as a way to monitor the quality of the openai models and the instructor library. To get started check out the [jxnl/instructor/tests/evals](https://github.com/jxnl/instructor/tree/main/tests/openai/evals) and contribute your own evals in the form of pytest tests. These evals will be run once a week and the results will be posted. +We invite you to contribute evals in pytest as a way to monitor the quality of the openai models and the instructor library. To get started check out the [jxnl/instructor/tests/llm/test_openai/evals](https://github.com/jxnl/instructor/tree/main/tests/llm/test_openai/evals) and contribute your own evals in the form of pytest tests. These evals will be run once a week and the results will be posted. ## Issues diff --git a/docs/examples/batch_classification.md b/docs/examples/batch_classification.md index 4ac37bdef..e8319a2d4 100644 --- a/docs/examples/batch_classification.md +++ b/docs/examples/batch_classification.md @@ -38,7 +38,7 @@ If you want to learn more about how to do bad computations, check out our post o import openai import instructor -client = instructor.patch( +client = instructor.from_openai( openai.AsyncOpenAI(), ) ``` diff --git a/docs/examples/classification.md b/docs/examples/classification.md index 74c92cf8e..898455ee1 100644 --- a/docs/examples/classification.md +++ b/docs/examples/classification.md @@ -42,7 +42,7 @@ import instructor # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def classify(data: str) -> SinglePrediction: diff --git a/docs/examples/document_segmentation.md b/docs/examples/document_segmentation.md new file mode 100644 index 000000000..e67260e37 --- /dev/null +++ b/docs/examples/document_segmentation.md @@ -0,0 +1,143 @@ +# Document Segmentation + +In this guide, we demonstrate how to do document segmentation using structured output from an LLM. We'll be using [command-r-plus](https://docs.cohere.com/docs/command-r-plus) - one of Cohere's latest LLMs with 128k context length and testing the approach on an article explaining the Transformer architecture. Same approach to document segmentation can be applied to any other domain where we need to break down a complex long document into smaller chunks. + +!!! tips "Motivation" + Sometimes we need a way to split the document into meaningful parts that center around a signle key concept/idea. Simple length-based / rule-based text-splitters are not reliable enough. Consider the cases where documents contain code snippets or math equations - we don't want to split those on `'\n\n'` or have to write extensive rules for different types of documents. It turns out that LLMs with sufficiently long context length are well suited for this task. + +## Defining the Data Structures + +First, we need to define a **`Section`** class for each of the document's segments. **`StructuredDocument`** class will then encapsulate a list of these sections. + +Note that in order to avoid LLM regenerating the content of each section, we can simply enumerate each line of the input document and then ask LLM to segment it by providing start-end line numbers for each section. + +```python +from pydantic import BaseModel, Field +from typing import List, Dict, Any + +class Section(BaseModel): + title: str = Field(description="main topic of this section of the document") + start_index: int = Field(description="line number where the section begins") + end_index: int = Field(description="line number where the section ends") + + +class StructuredDocument(BaseModel): + """obtains meaningful sections, each centered around a single concept/topic""" + sections: List[Section] = Field(description="a list of sections of the document") +``` + +## Document Preprocessing + +Preprocess the input `document` by prepending each line with its number. + +```python +def doc_with_lines(document): + document_lines = document.split("\n") + document_with_line_numbers = "" + line2text = {} + for i, line in enumerate(document_lines): + document_with_line_numbers += f"[{i}] {line}\n" + line2text[i] = line + return document_with_line_numbers, line2text +``` + +## Segmentation + +Next use a Cohere client to extract `StructuredDocument` from the preprocessed doc. + +```python +import instructor +import cohere + +# Apply the patch to the cohere client +# enables response_model keyword +client = instructor.from_cohere(cohere.Client()) + + +system_prompt = f"""\ +You are a world class educator working on organizing your lecture notes. +Read the document below and extract a StructuredDocument object from it where each section of the document is centered around a single concept/topic that can be taught in one lesson. +Each line of the document is marked with its line number in square brackets (e.g. [1], [2], [3], etc). Use the line numbers to indicate section start and end. +""" + + +def get_structured_document(document_with_line_numbers) -> StructuredDocument: + return client.chat.completions.create( + model="command-r-plus", + response_model=StructuredDocument, + messages=[ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": document_with_line_numbers, + }, + ], + ) # type: ignore +``` + + +Next, we need to get back the section text based on the start/end indices and our `line2text` dict from the preprocessing step. + +```python +def get_sections_text(structured_doc, line2text): + segments = [] + for s in structured_doc.sections: + contents = [] + for line_id in range(s.start_index, s.end_index): + contents.append(line2text.get(line_id, '')) + segments.append({ + "title": s.title, + "content": "\n".join(contents), + "start": s.start_index, + "end": s.end_index + }) + return segments +``` + + +## Example + +Here's an example of using these classes and functions to segment a tutorial on Transformers from [Sebastian Raschka](https://sebastianraschka.com/blog/2023/self-attention-from-scratch.html). We can use `trafilatura` package to scrape the web page content of the article. + +```python +from trafilatura import fetch_url, extract + + +url='https://sebastianraschka.com/blog/2023/self-attention-from-scratch.html' +downloaded = fetch_url(url) +document = extract(downloaded) + + +document_with_line_numbers, line2text = doc_with_lines(document) +structured_doc = get_structured_document(document_with_line_numbers) +segments = get_sections_text(structured_doc, line2text) +``` + +``` +print(segments[5]['title']) +""" +Introduction to Multi-Head Attention +""" +print(segments[5]['content']) +""" +Multi-Head Attention +In the very first figure, at the top of this article, we saw that transformers use a module called multi-head attention. How does that relate to the self-attention mechanism (scaled-dot product attention) we walked through above? +In the scaled dot-product attention, the input sequence was transformed using three matrices representing the query, key, and value. These three matrices can be considered as a single attention head in the context of multi-head attention. The figure below summarizes this single attention head we covered previously: +As its name implies, multi-head attention involves multiple such heads, each consisting of query, key, and value matrices. This concept is similar to the use of multiple kernels in convolutional neural networks. +To illustrate this in code, suppose we have 3 attention heads, so we now extend the \(d' \times d\) dimensional weight matrices so \(3 \times d' \times d\): +In: +h = 3 +multihead_W_query = torch.nn.Parameter(torch.rand(h, d_q, d)) +multihead_W_key = torch.nn.Parameter(torch.rand(h, d_k, d)) +multihead_W_value = torch.nn.Parameter(torch.rand(h, d_v, d)) +Consequently, each query element is now \(3 \times d_q\) dimensional, where \(d_q=24\) (here, let’s keep the focus on the 3rd element corresponding to index position 2): +In: +multihead_query_2 = multihead_W_query.matmul(x_2) +print(multihead_query_2.shape) +Out: +torch.Size([3, 24]) +""" +``` diff --git a/docs/examples/entity_resolution.md b/docs/examples/entity_resolution.md index b8393d8e3..59665318d 100644 --- a/docs/examples/entity_resolution.md +++ b/docs/examples/entity_resolution.md @@ -57,7 +57,7 @@ from openai import OpenAI # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def ask_ai(content) -> DocumentExtraction: diff --git a/docs/examples/exact_citations.md b/docs/examples/exact_citations.md index 72c5cb928..a2ffa268d 100644 --- a/docs/examples/exact_citations.md +++ b/docs/examples/exact_citations.md @@ -20,7 +20,7 @@ The `Fact` class encapsulates a single statement or fact. It contains two fields This method validates the sources (`substring_quote`) in the context. It utilizes regex to find the span of each substring quote in the given context. If the span is not found, the quote is removed from the list. ```python hl_lines="6 8-13" -from pydantic import Field, BaseModel, model_validator, FieldValidationInfo +from pydantic import Field, BaseModel, model_validator, ValidationInfo from typing import List @@ -29,7 +29,7 @@ class Fact(BaseModel): substring_quote: List[str] = Field(...) @model_validator(mode="after") - def validate_sources(self, info: FieldValidationInfo) -> "Fact": + def validate_sources(self, info: ValidationInfo) -> "Fact": text_chunks = info.context.get("text_chunk", None) spans = list(self.get_spans(text_chunks)) self.substring_quote = [text_chunks[span[0] : span[1]] for span in spans] @@ -80,7 +80,7 @@ import instructor # Apply the patch to the OpenAI client # enables response_model, validation_context keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def ask_ai(question: str, context: str) -> QuestionAnswer: diff --git a/docs/examples/examples.md b/docs/examples/examples.md new file mode 100644 index 000000000..c0378919b --- /dev/null +++ b/docs/examples/examples.md @@ -0,0 +1,63 @@ +# How should I include examples? + +To enhance the clarity and usability of your model and prompt, incorporating examples directly into the JSON schema extra of your Pydantic model is highly recommended. This approach not only streamlines the integration of practical examples but also ensures that they are easily accessible and understandable within the context of your model's schema. + + +```python +import openai +import instructor +from typing import Iterable +from pydantic import BaseModel, Field, ConfigDict + +client = instructor.from_openai(openai.OpenAI()) + + +class SyntheticQA(BaseModel): + question: str + answer: str + + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + {"question": "What is the capital of France?", "answer": "Paris"}, + { + "question": "What is the largest planet in our solar system?", + "answer": "Jupiter", + }, + { + "question": "Who wrote 'To Kill a Mockingbird'?", + "answer": "Harper Lee", + }, + { + "question": "What element does 'O' represent on the periodic table?", + "answer": "Oxygen", + }, + ] + } + ) + + +def get_synthetic_data() -> Iterable[SyntheticQA]: + return client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "Generate synthetic examples"}, + { + "role": "user", + "content": "Generate the exact examples you see in the examples of this prompt. ", + }, + ], + response_model=Iterable[SyntheticQA], + ) # type: ignore + + +if __name__ == "__main__": + for example in get_synthetic_data(): + print(example) + """ + question='What is the capital of France?' answer='Paris' + question='What is the largest planet in our solar system?' answer='Jupiter' + question="Who wrote 'To Kill a Mockingbird'?" answer='Harper Lee' + question="What element does 'O' represent on the periodic table?" answer='Oxygen' + """ +``` \ No newline at end of file diff --git a/docs/examples/extract_slides.md b/docs/examples/extract_slides.md index 4127399f5..58fcb2a51 100644 --- a/docs/examples/extract_slides.md +++ b/docs/examples/extract_slides.md @@ -56,7 +56,7 @@ from openai import OpenAI # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch( +client = instructor.from_openai( OpenAI(), mode=instructor.Mode.MD_JSON ) diff --git a/docs/examples/extracting_tables.md b/docs/examples/extracting_tables.md index 6d7fcff0f..bb6314736 100644 --- a/docs/examples/extracting_tables.md +++ b/docs/examples/extracting_tables.md @@ -65,7 +65,7 @@ from openai import OpenAI # Apply the patch to the OpenAI client to support response_model # Also use MD_JSON mode since the visino model does not support any special structured output mode -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) def extract_table(url: str) -> Iterable[Table]: diff --git a/docs/examples/groq.md b/docs/examples/groq.md new file mode 100644 index 000000000..0da64ce30 --- /dev/null +++ b/docs/examples/groq.md @@ -0,0 +1,67 @@ +# Structured Outputs using Groq +Instead of using openai or antrophic you can now also use groq for inference by using from_groq. + +The examples are using mixtral-8x7b model. + +## GroqCloud API +To use groq you need to obtain a groq API key. +Goto [groqcloud](https://console.groq.com) and login. Select API Keys from the left menu and then select Create API key to create a new key. + +## Use example +Some pip packages need to be installed to use the example: +``` +pip install instructor groq pydantic openai anthropic +``` +You need to export the groq API key: +``` +export GROQ_API_KEY= +``` + +An example: +```python +import os +from pydantic import BaseModel, Field +from typing import List +from groq import Groq +import instructor + +class Character(BaseModel): + name: str + fact: List[str] = Field(..., description="A list of facts about the subject") + + +client = Groq( + api_key=os.environ.get('GROQ_API_KEY'), +) + +client = instructor.from_groq(client, mode=instructor.Mode.TOOLS) + +resp = client.chat.completions.create( + model="mixtral-8x7b-32768", + messages=[ + { + "role": "user", + "content": "Tell me about the company Tesla", + } + ], + response_model=Character, +) +print(resp.model_dump_json(indent=2)) +""" +{ + "name": "Tesla", + "fact": [ + "An American electric vehicle and clean energy company.", + "Co-founded by Elon Musk, JB Straubel, Martin Eberhard, Marc Tarpenning, and Ian Wright in 2003.", + "Headquartered in Austin, Texas.", + "Produces electric vehicles, energy storage solutions, and more recently, solar energy products.", + "Known for its premium electric vehicles, such as the Model S, Model 3, Model X, and Model Y.", + "One of the world's most valuable car manufacturers by market capitalization.", + "Tesla's CEO, Elon Musk, is also the CEO of SpaceX, Neuralink, and The Boring Company.", + "Tesla operates the world's largest global network of electric vehicle supercharging stations.", + "The company aims to accelerate the world's transition to sustainable transport and energy through innovative technologies and products." + ] +} +""" +``` +You can find another example called groq_example2.py under examples/groq of this repository. \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md index 7998fb0e4..26feb1eea 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -17,5 +17,7 @@ 13. [How to generate advertising copy from image inputs](image_to_ad_copy.md) 14. [How to use local models from Ollama](ollama.md) 15. [How to store responses in a database with SQLModel](sqlmodel.md) +16. [How to use groqcloud api](groq.md) +17. [How to do document segmentation using LLMs?](document_segmentation.md) Explore more! diff --git a/docs/examples/knowledge_graph.md b/docs/examples/knowledge_graph.md index 5bdd7a754..d83fdfe53 100644 --- a/docs/examples/knowledge_graph.md +++ b/docs/examples/knowledge_graph.md @@ -43,7 +43,7 @@ import instructor # Adds response_model to ChatCompletion # Allows the return of Pydantic model rather than raw JSON -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def generate_graph(input) -> KnowledgeGraph: @@ -91,7 +91,7 @@ This will produce a visual representation of the knowledge graph, stored as "kno ## Iterative Updates -Now that we've seen how to generate a knowledge graph from a single input, let's see how we can iteratively update our knowledge graph with new information, or when informatino does not fit into a single prompt. +Now that we've seen how to generate a knowledge graph from a single input, let's see how we can iteratively update our knowledge graph with new information, or when information does not fit into a single prompt. Let's take an easy example where we want to visualise the combined knowledge graph that the following sentences represent. diff --git a/docs/examples/mistral.md b/docs/examples/mistral.md new file mode 100644 index 000000000..f5bc6b667 --- /dev/null +++ b/docs/examples/mistral.md @@ -0,0 +1,49 @@ +# Structured Outputs using Mistral +You can now also use mistralai models for inference by using from_mistral. + +The examples are using mistral-large-latest. + +## MistralAI API +To use mistral you need to obtain a mistral API key. +Goto [mistralai](https://mistral.ai/) click on Build Now and login. Select API Keys from the left menu and then select +Create API key to create a new key. + +## Use example +Some pip packages need to be installed to use the example: +``` +pip install instructor mistralai pydantic +``` +You need to export the mistral API key: +``` +export MISTRAL_API_KEY= +``` + +An example: +```python +import os +from pydantic import BaseModel, Field +from typing import List +from mistralai.client import MistralClient +from instructor import from_mistral, Mode + +class UserDetails(BaseModel): + name: str + age: int + + +# enables `response_model` in chat call +client = MistralClient(api_key=os.environ.get("MISTRAL_API_KEY")) + +instructor_client = from_mistral(client=client, model="mistral-large-latest", + mode=Mode.MISTRAL_TOOLS, max_tokens=1000) + +resp = instructor_client.messages.create( + response_model=UserDetails, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, +) + +print(resp) + +# output: UserDetails(name='Jason', age=10) +``` diff --git a/docs/examples/moderation.md b/docs/examples/moderation.md index 535c52e8f..dbaac805e 100644 --- a/docs/examples/moderation.md +++ b/docs/examples/moderation.md @@ -23,7 +23,7 @@ from typing_extensions import Annotated from pydantic import BaseModel, AfterValidator from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Response(BaseModel): diff --git a/docs/examples/ollama.md b/docs/examples/ollama.md index 18bb5afb1..305357334 100644 --- a/docs/examples/ollama.md +++ b/docs/examples/ollama.md @@ -40,7 +40,7 @@ class Character(BaseModel): # enables `response_model` in create call -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="http://localhost:11434/v1", api_key="ollama", # required, but unused diff --git a/docs/examples/pii.md b/docs/examples/pii.md index 90fc294b0..694b2eadf 100644 --- a/docs/examples/pii.md +++ b/docs/examples/pii.md @@ -45,7 +45,7 @@ The OpenAI API is utilized to extract PII information from a given document. from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) EXAMPLE_DOCUMENT = """ # Fake Document with PII for Testing PII Scrubbing Model diff --git a/docs/examples/planning-tasks.md b/docs/examples/planning-tasks.md index 38ea10074..bd80c90d0 100644 --- a/docs/examples/planning-tasks.md +++ b/docs/examples/planning-tasks.md @@ -76,7 +76,7 @@ from openai import OpenAI # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def query_planner(question: str) -> QueryPlan: diff --git a/docs/examples/search.md b/docs/examples/search.md index 998489143..3940b9ca0 100644 --- a/docs/examples/search.md +++ b/docs/examples/search.md @@ -13,12 +13,12 @@ The `Search` class is a Pydantic model that defines the structure of the search ```python import instructor from openai import OpenAI -from typing import Iterable +from typing import Iterable, Literal from pydantic import BaseModel, Field # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Search(BaseModel): query: str = Field(..., description="Query to search for relevant content") @@ -30,7 +30,7 @@ class Search(BaseModel): ) -def segment(data: str) -> MultiSearch: +def segment(data: str) -> Search: return client.chat.completions.create( model="gpt-3.5-turbo-0613", response_model=Iterable[Search], diff --git a/docs/examples/self_critique.md b/docs/examples/self_critique.md index a620dd6fc..063e976f9 100644 --- a/docs/examples/self_critique.md +++ b/docs/examples/self_critique.md @@ -11,7 +11,7 @@ import instructor # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class QuestionAnswer(BaseModel): question: str @@ -56,13 +56,17 @@ Lets integrate `llm_validator` into the model and see the error message. Its imp from pydantic import BaseModel, BeforeValidator from typing_extensions import Annotated from instructor import llm_validator +from openai import OpenAI +import instructor + +client = instructor.from_openai(OpenAI()) class QuestionAnswerNoEvil(BaseModel): question: str answer: Annotated[ str, BeforeValidator( - llm_validator("don't say objectionable things", allow_override=True) + llm_validator("don't say objectionable things", client=client, allow_override=True) ), ] diff --git a/docs/examples/sqlmodel.md b/docs/examples/sqlmodel.md index c45c6cbc3..eaf82f0b4 100644 --- a/docs/examples/sqlmodel.md +++ b/docs/examples/sqlmodel.md @@ -31,7 +31,7 @@ class Hero(SQLModel, instructor.OpenAISchema, table=True): The `create_hero` function will query `OpenAI` for a `Hero` record ```python -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def create_hero() -> Hero: return client.chat.completions.create( diff --git a/docs/examples/watsonx.md b/docs/examples/watsonx.md new file mode 100644 index 000000000..52481966a --- /dev/null +++ b/docs/examples/watsonx.md @@ -0,0 +1,68 @@ +# Structured Outputs with IBM watsonx.ai + +You can use IBM watsonx.ai for inference using [LiteLLM](https://docs.litellm.ai/docs/providers/watsonx). + +## Prerequisites + +- IBM Cloud Account +- API Key from IBM Cloud IAM: https://cloud.ibm.com/iam/apikeys +- Project ID (from watsonx.ai instance URL: https://dataplatform.cloud.ibm.com/projects//) + +## Install + +```bash +poetry install instructor --with litellm +``` + +## Example + +```python +import os + +import litellm +from litellm import completion +from pydantic import BaseModel, Field + +import instructor +from instructor import Mode + +litellm.drop_params = True # watsonx.ai doesn't support `json_mode` + +os.environ["WATSONX_URL"] = "https://us-south.ml.cloud.ibm.com" +os.environ["WATSONX_API_KEY"] = "" +os.environ["WATSONX_PROJECT_ID"] = "" +# Additional options: https://docs.litellm.ai/docs/providers/watsonx + + +class Company(BaseModel): + name: str = Field(description="name of the company") + year_founded: int = Field(description="year the company was founded") + + +client = instructor.from_litellm(completion, mode=Mode.JSON) + +resp = client.chat.completions.create( + model="watsonx/meta-llama/llama-3-8b-instruct", + max_tokens=1024, + messages=[ + { + "role": "user", + "content": """\ +Given the following text, create a Company object: + +IBM was founded in 1911 as the Computing-Tabulating-Recording Company (CTR), a holding company of manufacturers of record-keeping and measuring systems. +""", + } + ], + project_id=os.environ["WATSONX_PROJECT_ID"], + response_model=Company, +) + +print(resp.model_dump_json(indent=2)) +""" +{ + "name": "IBM", + "year_founded": 1911 +} +""" +``` diff --git a/docs/hub/action_items.md b/docs/hub/action_items.md index bf0d7a6b4..542b1834f 100644 --- a/docs/hub/action_items.md +++ b/docs/hub/action_items.md @@ -61,7 +61,7 @@ class Ticket(BaseModel): # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def generate(data: str) -> Iterable[Ticket]: diff --git a/docs/hub/anthropic.md b/docs/hub/anthropic.md new file mode 100644 index 000000000..1d89938f8 --- /dev/null +++ b/docs/hub/anthropic.md @@ -0,0 +1,69 @@ +# Anthropic + +Now that we have a [Anthropic](https://www.anthropic.com/) client, we can use it with the `instructor` client to make requests. + +``` +pip install anthropic +``` + +```python +from pydantic import BaseModel +from typing import List +import anthropic +import instructor + +# Patching the Anthropics client with the instructor for enhanced capabilities +client = instructor.from_anthropic( + anthropic.Anthropic(), +) + + +class Properties(BaseModel): + name: str + value: str + + +class User(BaseModel): + name: str + age: int + properties: List[Properties] + + +# client.messages.create will also work due to the instructor client +user_response = client.chat.completions.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name, age, and properties.", + } + ], + response_model=User, +) # type: ignore + +print(user_response.model_dump_json(indent=2)) +""" +{ + "name": "John Doe", + "age": 35, + "properties": [ + { + "name": "Occupation", + "value": "Software Engineer" + }, + { + "name": "Hobbies", + "value": "Reading, Hiking, Cooking" + }, + { + "name": "Location", + "value": "San Francisco, CA" + } + ] +} +""" +``` + +We're encountering challenges with deeply nested types and eagerly invite the community to test, provide feedback, and suggest necessary improvements as we enhance the anthropic client's support. \ No newline at end of file diff --git a/docs/hub/anyscale.md b/docs/hub/anyscale.md index 55450266d..d61abdff6 100644 --- a/docs/hub/anyscale.md +++ b/docs/hub/anyscale.md @@ -58,7 +58,7 @@ class UserDetails(BaseModel): # enables `response_model` in create call -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="https://api.endpoints.anyscale.com/v1", api_key=os.environ["ANYSCALE_API_KEY"], diff --git a/docs/hub/batch_classification_langsmith.md b/docs/hub/batch_classification_langsmith.md index f26e017c1..4d337aab5 100644 --- a/docs/hub/batch_classification_langsmith.md +++ b/docs/hub/batch_classification_langsmith.md @@ -42,11 +42,12 @@ from enum import Enum client = wrap_openai(AsyncOpenAI()) # Patch the client with instructor -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client) # Rate limit the number of requests sem = asyncio.Semaphore(5) + # Use an Enum to define the types of questions class QuestionType(Enum): CONTACT = "CONTACT" diff --git a/docs/hub/cohere.md b/docs/hub/cohere.md new file mode 100644 index 000000000..7f6fa7e72 --- /dev/null +++ b/docs/hub/cohere.md @@ -0,0 +1,85 @@ +# Structured Outputs with Cohere + +If you want to try this example using `instructor hub`, you can pull it by running + +```bash +instructor hub pull --slug cohere --py > cohere_example.py +``` + +You can now use any of the Cohere's [command models](https://docs.cohere.com/docs/models) with the `instructor` library to get structured outputs. + +You'll need a cohere API key which can be obtained by signing up [here](https://dashboard.cohere.com/) and gives you [free](https://cohere.com/pricing), rate-limited usage for learning and prototyping. + +## Setup +``` +pip install cohere +``` +Export your key: +``` +export CO_API_KEY= +``` + +## Example + +```python +from pydantic import BaseModel, Field +from typing import List +import cohere +import instructor + + +# Patching the Cohere client with the instructor for enhanced capabilities +client = instructor.from_cohere( + cohere.Client(), + max_tokens=1000, + model="command-r-plus", +) + + +class Person(BaseModel): + name: str = Field(description="name of the person") + country_of_origin: str = Field(description="country of origin of the person") + + +class Group(BaseModel): + group_name: str = Field(description="name of the group") + members: List[Person] = Field(description="list of members in the group") + + +task = """\ +Given the following text, create a Group object for 'The Beatles' band + +Text: +The Beatles were an English rock band formed in Liverpool in 1960. With a line-up comprising John Lennon, Paul McCartney, George Harrison and Ringo Starr, they are regarded as the most influential band of all time. The group were integral to the development of 1960s counterculture and popular music's recognition as an art form. +""" +group = client.messages.create( + response_model=Group, + messages=[{"role": "user", "content": task}], + temperature=0, +) + +print(group.model_dump_json(indent=2)) +""" +{ + "group_name": "The Beatles", + "members": [ + { + "name": "John Lennon", + "country_of_origin": "England" + }, + { + "name": "Paul McCartney", + "country_of_origin": "England" + }, + { + "name": "George Harrison", + "country_of_origin": "England" + }, + { + "name": "Ringo Starr", + "country_of_origin": "England" + } + ] +} +""" +``` \ No newline at end of file diff --git a/docs/hub/extract_contact_info.md b/docs/hub/extract_contact_info.md index 2bbc7462c..68e903095 100644 --- a/docs/hub/extract_contact_info.md +++ b/docs/hub/extract_contact_info.md @@ -41,7 +41,7 @@ class Lead(BaseModel): # Can define some function here to send Lead information to a database using an API -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def parse_lead_from_message(user_message: str): diff --git a/docs/hub/groq.md b/docs/hub/groq.md new file mode 100644 index 000000000..4437e39fb --- /dev/null +++ b/docs/hub/groq.md @@ -0,0 +1,83 @@ +# Structured Outputs with Groq AI + +If you want to try this example using `instructor hub`, you can pull it by running + +```bash +instructor hub pull --slug groq --py > groq_example.py +``` + +you'll need to sign up for an account and get an API key. You can do that [here](https://console.groq.com/docs/quickstart). + +```bash +export GROQ_API_KEY= +pip install groq +``` + +!!! note "Other Languages" + + This blog post is written in Python, but the concepts are applicable to other languages as well, as we currently have support for [Javascript](https://instructor-ai.github.io/instructor-js), [Elixir](https://hexdocs.pm/instructor/Instructor.html) and [PHP](https://github.com/cognesy/instructor-php/). + + + +## Patching + +Instructor's patch enhances the openai api it with the following features: + +- `response_model` in `create` calls that returns a pydantic model +- `max_retries` in `create` calls that retries the call if it fails by using a backoff strategy + +!!! note "Learn More" + + To learn more, please refer to the [docs](../index.md). To understand the benefits of using Pydantic with Instructor, visit the tips and tricks section of the [why use Pydantic](../why.md) page. + +## Groq AI + +While Groq AI does not support function calling directly, you can still leverage the TOOLS mode for structured outputs. + +!!! note "Getting access" + + If you want to try this out for yourself check out the [docs](https://console.groq.com/docs/quickstart) + + +```python +import os +import instructor + +from groq import Groq +from pydantic import BaseModel + +client = Groq( + api_key=os.environ.get("GROQ_API_KEY"), +) + +# By default, the patch function will patch the ChatCompletion.create and ChatCompletion.create methods to support the response_model parameter +client = instructor.from_groq(client, mode=instructor.Mode.TOOLS) + + +# Now, we can use the response_model parameter using only a base model +# rather than having to use the OpenAISchema class +class UserExtract(BaseModel): + name: str + age: int + + +user: UserExtract = client.chat.completions.create( + model="mixtral-8x7b-32768", + response_model=UserExtract, + messages=[ + {"role": "user", "content": "Extract jason is 25 years old"}, + ], +) + +assert isinstance(user, UserExtract), "Should be instance of UserExtract" +assert user.name.lower() == "jason" +assert user.age == 25 + +print(user.model_dump_json(indent=2)) +""" +{ + "name": "jason", + "age": 25 +} +""" +``` diff --git a/docs/hub/img/youtube.gif b/docs/hub/img/youtube.gif new file mode 100644 index 000000000..d763c1f5a Binary files /dev/null and b/docs/hub/img/youtube.gif differ diff --git a/docs/hub/knowledge_graph.md b/docs/hub/knowledge_graph.md index cebf6c6c3..f2ae45e94 100644 --- a/docs/hub/knowledge_graph.md +++ b/docs/hub/knowledge_graph.md @@ -34,7 +34,7 @@ class KnowledgeGraph(BaseModel): # Patch the OpenAI client to add response_model support -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def generate_graph(input_text: str) -> KnowledgeGraph: @@ -48,45 +48,46 @@ def generate_graph(input_text: str) -> KnowledgeGraph: } ], response_model=KnowledgeGraph, - ) # type: ignore + ) if __name__ == "__main__": input_text = "Jason is Sarah's friend and he is a doctor" graph = generate_graph(input_text) - print(graph.json(indent=2)) + print(graph.model_dump_json(indent=2)) """ { - "nodes": [ - { - "id": 1, - "label": "Jason", - "color": "blue" - }, - { - "id": 2, - "label": "Sarah", - "color": "red" - }, - { - "id": 3, - "label": "Doctor", - "color": "green" - } - ], - "edges": [ - { - "source": 1, - "target": 2, - "label": "Friend", - "color": "black" - }, - { - "source": 1, - "target": 3, - "label": "Profession", - "color": "black" - } - ] + "nodes": [ + { + "id": 1, + "label": "Jason", + "color": "blue" + }, + { + "id": 2, + "label": "Sarah", + "color": "blue" + }, + { + "id": 3, + "label": "Doctor", + "color": "blue" + } + ], + "edges": [ + { + "source": 1, + "target": 2, + "label": "friend", + "color": "black" + }, + { + "source": 1, + "target": 3, + "label": "is a", + "color": "black" + } + ] } + """ ``` \ No newline at end of file diff --git a/docs/hub/mistral.md b/docs/hub/mistral.md index 74b7ad97d..d1fd4e0b7 100644 --- a/docs/hub/mistral.md +++ b/docs/hub/mistral.md @@ -50,12 +50,10 @@ from mistralai.client import MistralClient # enables `response_model` in chat call client = MistralClient() -patched_chat = instructor.patch( - create=client.chat, - mode=instructor.Mode.MISTRAL_TOOLS -) +patched_chat = instructor.from_openai(create=client.chat, mode=instructor.Mode.MISTRAL_TOOLS) if __name__ == "__main__": + class UserDetails(BaseModel): name: str age: int diff --git a/docs/hub/multiple_classification.md b/docs/hub/multiple_classification.md index 1ce445be0..69fe0943e 100644 --- a/docs/hub/multiple_classification.md +++ b/docs/hub/multiple_classification.md @@ -15,7 +15,7 @@ from pydantic import BaseModel, Field # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) LABELS = Literal["ACCOUNT", "BILLING", "GENERAL_QUERY"] diff --git a/docs/hub/ollama.md b/docs/hub/ollama.md index 8dcaa8c58..4b499befe 100644 --- a/docs/hub/ollama.md +++ b/docs/hub/ollama.md @@ -59,7 +59,7 @@ class Character(BaseModel): # enables `response_model` in create call -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="http://localhost:11434/v1", api_key="ollama", # required, but unused diff --git a/docs/hub/pandas_df.md b/docs/hub/pandas_df.md index 3148f4967..82f3225de 100644 --- a/docs/hub/pandas_df.md +++ b/docs/hub/pandas_df.md @@ -58,7 +58,7 @@ MarkdownDataFrame = Annotated[ ), ] -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) def extract_df(data: str) -> pd.DataFrame: @@ -108,13 +108,13 @@ if __name__ == "__main__": assert isinstance(df, pd.DataFrame) print(df) """ - Party Years Served + Party Years Served President - Joe Biden Democratic 2021- - Donald Trump Republican 2017-2021 - Barack Obama Democratic 2009-2017 - George W. Bush Republican 2001-2009 - Bill Clinton Democratic 1993-2001 + Joe Biden Democratic 2021 - Present + Donald Trump Republican 2017 - 2021 + Barack Obama Democratic 2009 - 2017 + George W. Bush Republican 2001 - 2009 + Bill Clinton Democratic 1993 - 2001 """ table = extract_table( @@ -129,11 +129,11 @@ if __name__ == "__main__": """ Party Years Served President - Joe Biden Democrat 2021 - Present + Joe Biden Democratic 2021 - Present Donald Trump Republican 2017 - 2021 - Barack Obama Democrat 2009 - 2017 + Barack Obama Democratic 2009 - 2017 George W. Bush Republican 2001 - 2009 - Bill Clinton Democrat 1993 - 2001 + Bill Clinton Democratic 1993 - 2001 """ ``` diff --git a/docs/hub/partial_streaming.md b/docs/hub/partial_streaming.md index 899ad1ac2..5b937675a 100644 --- a/docs/hub/partial_streaming.md +++ b/docs/hub/partial_streaming.md @@ -16,7 +16,7 @@ from openai import OpenAI from pydantic import BaseModel from typing import List -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) text_block = """ In our recent online meeting, participants from various backgrounds joined to discuss the upcoming tech conference. The names and contact details of the participants were as follows: diff --git a/docs/hub/single_classification.md b/docs/hub/single_classification.md index a0d3b4885..2a30010e5 100644 --- a/docs/hub/single_classification.md +++ b/docs/hub/single_classification.md @@ -16,7 +16,7 @@ import instructor # Apply the patch to the OpenAI client # enables response_model keyword -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class ClassificationResponse(BaseModel): diff --git a/docs/hub/tables_from_vision.md b/docs/hub/tables_from_vision.md index 114b4c484..777f88827 100644 --- a/docs/hub/tables_from_vision.md +++ b/docs/hub/tables_from_vision.md @@ -21,9 +21,13 @@ from pydantic import ( ) import instructor import pandas as pd +from rich.console import Console - -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) +console = Console() +client = instructor.from_openai( + client=OpenAI(), + mode=instructor.Mode.TOOLS, +) def md_to_df(data: Any) -> Any: @@ -37,7 +41,7 @@ def md_to_df(data: Any) -> Any: .dropna(axis=1, how="all") .iloc[1:] .map(lambda x: x.strip()) - ) + ) # type: ignore return data @@ -49,7 +53,7 @@ MarkdownDataFrame = Annotated[ { "type": "string", "description": """ - The markdown representation of the table, + The markdown representation of the table, each one should be tidy, do not try to join tables that should be seperate""", } @@ -83,18 +87,14 @@ example = MultipleTables( def extract(url: str) -> MultipleTables: - tables = client.chat.completions.create( - model="gpt-4-vision-preview", + return client.chat.completions.create( + model="gpt-4-turbo", max_tokens=4000, response_model=MultipleTables, messages=[ { "role": "user", "content": [ - { - "type": "text", - "text": f"Describe this data accurately as a table in markdown format. {example.model_dump_json(indent=2)}", - }, { "type": "image_url", "image_url": {"url": url}, @@ -102,11 +102,12 @@ def extract(url: str) -> MultipleTables: { "type": "text", "text": """ - First take a moment to reason about the best set of headers for the tables. - Write a good h1 for the image above. Then follow up with a short description of the what the data is about. - Then for each table you identified, write a h2 tag that is a descriptive title of the table. - Then follow up with a short description of the what the data is about. - Lastly, produce the markdown table for each table you identified. + First, analyze the image to determine the most appropriate headers for the tables. + Generate a descriptive h1 for the overall image, followed by a brief summary of the data it contains. + For each identified table, create an informative h2 title and a concise description of its contents. + Finally, output the markdown representation of each table. + + Make sure to escape the markdown table properly, and make sure to include the caption and the dataframe. including escaping all the newlines and quotes. Only return a markdown table in dataframe, nothing else. """, @@ -115,45 +116,14 @@ def extract(url: str) -> MultipleTables: } ], ) - return tables -if __name__ == "__main__": - urls = [ - "https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png/m/2880x0", - ] - for url in urls: - tables = extract(url) - for table in tables.tables: - print(table.caption) - #> Top 10 Grossing Android Apps - """ - App Name Category - Rank - 1 Google One Productivity - 2 Disney+ Entertainment - 3 TikTok - Videos, Music & LIVE Entertainment - 4 Candy Crush Saga Games - 5 Tinder: Dating, Chat & Friends Social networking - 6 Coin Master Games - 7 Roblox Games - 8 Bumble - Dating & Make Friends Dating - 9 Royal Match Games - 10 Spotify: Music and Podcasts Music & Audio - """ - print(table.dataframe) - """ - App Name Category - Rank - 1 Tinder: Dating, Chat & Friends Social networking - 2 Disney+ Entertainment - 3 YouTube: Watch, Listen, Stream Entertainment - 4 Audible: Audio Entertainment Entertainment - 5 Candy Crush Saga Games - 6 TikTok - Videos, Music & LIVE Entertainment - 7 Bumble - Dating & Make Friends Dating - 8 Roblox Games - 9 LinkedIn: Job Search & News Business - 10 Duolingo - Language Lessons Education - """ +urls = [ + "https://a.storyblok.com/f/47007/2400x1260/f816b031cb/uk-ireland-in-three-charts_chart_a.png/m/2880x0", + "https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png/m/2880x0", +] + +for url in urls: + for table in extract(url).tables: + console.print(table.caption, "\n", table.dataframe) ``` diff --git a/docs/hub/together.md b/docs/hub/together.md index 49ce483aa..252179f37 100644 --- a/docs/hub/together.md +++ b/docs/hub/together.md @@ -60,7 +60,7 @@ client = openai.OpenAI( # By default, the patch function will patch the ChatCompletion.create and ChatCompletion.create methods to support the response_model parameter -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) # Now, we can use the response_model parameter using only a base model diff --git a/docs/hub/youtube_clips.md b/docs/hub/youtube_clips.md new file mode 100644 index 000000000..2e5c88412 --- /dev/null +++ b/docs/hub/youtube_clips.md @@ -0,0 +1,125 @@ +# Generating YouTube Clips from Transcripts + +This guide demonstrates how to generate concise, informative clips from YouTube video transcripts using the `instructor` library. By leveraging the power of OpenAI's models, we can extract meaningful segments from a video's transcript, which can then be recut into smaller, standalone videos. This process involves identifying key moments within a transcript and summarizing them into clips with specific titles and descriptions. + +If you're interested in trying this example using `instructor hub`, you can pull it by running: + + +```bash +pip install youtube_transcript_api instructor rich +instructor hub pull --slug youtube-clips --py > youtube_clips.py +``` + +![youtube clip streaming](./img/youtube.gif) + +```python +from youtube_transcript_api import YouTubeTranscriptApi +from pydantic import BaseModel, Field +from typing import List, Generator, Iterable +import instructor +import openai + +client = instructor.from_openai(openai.OpenAI()) + + +def extract_video_id(url: str) -> str | None: + import re + + match = re.search(r"v=([a-zA-Z0-9_-]+)", url) + if match: + return match.group(1) + + +class TranscriptSegment(BaseModel): + source_id: int + start: float + text: str + + +def get_transcript_with_timing( + video_id: str, +) -> Generator[TranscriptSegment, None, None]: + """ + Fetches the transcript of a YouTube video along with the start and end times + for each text segment, and returns them as a list of Pydantic models. + """ + transcript = YouTubeTranscriptApi.get_transcript(video_id) + for ii, segment in enumerate(transcript): + yield TranscriptSegment( + source_id=ii, start=segment["start"], text=segment["text"] + ) + + +class YoutubeClip(BaseModel): + title: str = Field(description="Specific and informative title for the clip.") + description: str = Field( + description="A detailed description of the clip, including notable quotes or phrases." + ) + start: float + end: float + + +class YoutubeClips(BaseModel): + clips: List[YoutubeClip] + + +def yield_clips(segments: Iterable[TranscriptSegment]) -> Iterable[YoutubeClips]: + return client.chat.completions.create( + model="gpt-4-turbo-preview", + stream=True, + messages=[ + { + "role": "system", + "content": """You are given a sequence of YouTube transcripts and your job + is to return notable clips that can be recut as smaller videos. Give very + specific titles and descriptions. Make sure the length of clips is proportional + to the length of the video. Note that this is a transcript and so there might + be spelling errors. Note that and correct any spellings. Use the context to + make sure you're spelling things correctly.""", + }, + { + "role": "user", + "content": f"Let's use the following transcript segments.\n{segments}", + }, + ], + response_model=instructor.Partial[YoutubeClips], + validation_context={"segments": segments}, + ) # type: ignore + + +# Example usage +if __name__ == "__main__": + from rich.table import Table + from rich.console import Console + from rich.prompt import Prompt + + console = Console() + url = Prompt.ask("Enter a YouTube URL") + + with console.status("[bold green]Processing YouTube URL...") as status: + video_id = extract_video_id(url) + + if video_id is None: + raise ValueError("Invalid YouTube video URL") + + transcript = list(get_transcript_with_timing(video_id)) + status.update("[bold green]Generating clips...") + + for clip in yield_clips(transcript): + console.clear() + + table = Table(title="Extracted YouTube Clips", padding=(0, 1)) + + table.add_column("Title", style="cyan") + table.add_column("Description", style="magenta") + table.add_column("Start", justify="right", style="green") + table.add_column("End", justify="right", style="green") + for youtube_clip in clip.clips or []: + table.add_row( + youtube_clip.title, + youtube_clip.description, + str(youtube_clip.start), + str(youtube_clip.end), + ) + console.print(table) +``` diff --git a/docs/index.md b/docs/index.md index 9dd0e0f55..75a0be817 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,165 +7,319 @@ _Structured outputs powered by llms. Designed for simplicity, transparency, and [![Twitter Follow](https://img.shields.io/twitter/follow/jxnlco?style=social)](https://twitter.com/jxnlco) [![Discord](https://img.shields.io/discord/1192334452110659664?label=discord)](https://discord.gg/CV8sPM5k5Y) [![Downloads](https://img.shields.io/pypi/dm/instructor.svg)](https://pypi.python.org/pypi/instructor) +[![GPT](https://img.shields.io/badge/docs-InstructorGPT-blue)](https://chat.openai.com/g/g-EvZweRWrE-instructor-gpt) Instructor makes it easy to reliably get structured data like JSON from Large Language Models (LLMs) like GPT-3.5, GPT-4, GPT-4-Vision, including open source models like Mistral/Mixtral from [Together](./hub/together.md), [Anyscale](./hub/anyscale.md), [Ollama](./hub/ollama.md), and [llama-cpp-python](./hub/llama-cpp-python.md). By leveraging various modes like Function Calling, Tool Calling and even constrained sampling modes like JSON mode, JSON Schema; Instructor stands out for its simplicity, transparency, and user-centric design. We leverage Pydantic to do the heavy lifting, and we've built a simple, easy-to-use API on top of it by helping you manage [validation context](./concepts/reask_validation.md), retries with [Tenacity](./concepts/retrying.md), and streaming [Lists](./concepts/lists.md) and [Partial](./concepts/partial.md) responses. -We also provide library in [Typescript](https://instructor-ai.github.io/instructor-js/), [Elixir](https://github.com/thmsmlr/instructor_ex/) and [PHP](https://github.com/cognesy/instructor-php/). -## Usage +We also provide a library in [Typescript](https://instructor-ai.github.io/instructor-js/), [Elixir](https://github.com/thmsmlr/instructor_ex/) and [PHP](https://github.com/cognesy/instructor-php/). -```py +## Why use Instructor? + +The question of using Instructor is fundamentally a question of why to use Pydantic. + +1. **Powered by type hints** — Instructor is powered by Pydantic, which is powered by type hints. Schema validation, prompting is controlled by type annotations; less to learn, less code to write, and integrates with your IDE. + +2. **Customizable** — Pydantic is highly customizable. You can define your own validators, custom error messages, and more. + +3. **Ecosystem** Pydantic is the most widely used data validation library for Python with over 100M downloads a month. It's used by FastAPI, Typer, and many other popular libraries. + + +## Getting Started + +``` +pip install -U instructor +``` + +If you ever get stuck, you can always run `instructor docs` to open the documentation in your browser. It even supports searching for specific topics. + +``` +instructor docs [QUERY] +``` + +You can also check out our [cookbooks](./examples/index.md) and [concepts](./concepts/models.md) to learn more about how to use Instructor. + +Now, let's see Instructor in action with a simple example: + +### Using OpenAI + +```python import instructor +from pydantic import BaseModel from openai import OpenAI + + +# Define your desired output structure +class UserInfo(BaseModel): + name: str + age: int + + +# Patch the OpenAI client +client = instructor.from_openai(OpenAI()) + +# Extract structured data from natural language +user_info = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserInfo, + messages=[{"role": "user", "content": "John Doe is 30 years old."}], +) + +print(user_info.name) +#> John Doe +print(user_info.age) +#> 30 +``` + +### Using Anthropic + +```python +import instructor +from anthropic import Anthropic from pydantic import BaseModel -# This enables response_model keyword -# from client.chat.completions.create -client = instructor.patch(OpenAI()) +class User(BaseModel): + name: str + age: int + + +client = instructor.from_anthropic(Anthropic()) + +# note that client.chat.completions.create will also work +resp = client.messages.create( + model="claude-3-opus-20240229", + max_tokens=1024, + messages=[ + { + "role": "user", + "content": "Extract Jason is 25 years old.", + } + ], + response_model=User, +) + +assert isinstance(resp, User) +assert resp.name == "Jason" +assert resp.age == 25 +``` + +### Using Litellm + +```python +import instructor +from litellm import completion +from pydantic import BaseModel -class UserDetail(BaseModel): + +class User(BaseModel): name: str age: int -user = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetail, +client = instructor.from_litellm(completion) + +resp = client.chat.completions.create( + model="claude-3-opus-20240229", + max_tokens=1024, messages=[ - {"role": "user", "content": "Extract Jason is 25 years old"}, + { + "role": "user", + "content": "Extract Jason is 25 years old.", + } ], + response_model=User, ) -assert isinstance(user, UserDetail) -assert user.name == "Jason" -assert user.age == 25 -print(user.model_dump_json(indent=2)) -""" -{ - "name": "Jason", - "age": 25 -} -""" +assert isinstance(resp, User) +assert resp.name == "Jason" +assert resp.age == 25 ``` -**Using async clients** +## Correct Typing -For async clients you must use `apatch` vs `patch` like so: +This was the dream of instructor but due to the patching of openai, it wasnt possible for me to get typing to work well. Now, with the new client, we can get typing to work well! We've also added a few `create_*` methods to make it easier to create iterables and partials, and to access the original completion. -```py -import asyncio +### Calling `create` + +```python +import openai import instructor -from openai import AsyncOpenAI from pydantic import BaseModel -aclient = instructor.apatch(AsyncOpenAI()) - -class UserExtract(BaseModel): +class User(BaseModel): name: str age: int -task = aclient.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserExtract, +client = instructor.from_openai(openai.OpenAI()) + +user = client.chat.completions.create( + model="gpt-4-turbo-preview", messages=[ - {"role": "user", "content": "Extract jason is 25 years old"}, + {"role": "user", "content": "Create a user"}, ], + response_model=User, ) - -response = asyncio.run(task) -print(response.model_dump_json(indent=2)) -""" -{ - "name": "Jason", - "age": 25 -} -""" ``` -!!! note "Accessing the original response and usage tokens" +Now if you use a IDE, you can see the type is correctly infered. - If you want to access anything like usage or other metadata, the original response is available on the `Model._raw_response` attribute. +![type](./blog/posts/img/type.png) - ```python - import openai - import instructor - from pydantic import BaseModel +### Handling async: `await create` - client = instructor.patch(openai.OpenAI()) +This will also work correctly with asynchronous clients. +```python +import openai +import instructor +from pydantic import BaseModel - class UserDetail(BaseModel): - name: str - age: int +client = instructor.from_openai(openai.AsyncOpenAI()) - user = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetail, + +class User(BaseModel): + name: str + age: int + + +async def extract(): + return await client.chat.completions.create( + model="gpt-4-turbo-preview", messages=[ - {"role": "user", "content": "Extract Jason is 25 years old"}, + {"role": "user", "content": "Create a user"}, ], + response_model=User, ) +``` - print(user._raw_response.model_dump_json(indent=2)) - """ - { - "id": "chatcmpl-8u9e2TV3ehCgLsRxNLLeAbzpEmBuZ", - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "logprobs": null, - "message": { - "content": null, - "role": "assistant", - "function_call": null, - "tool_calls": [ - { - "id": "call_3ZuQhfteTLEy7CUokjwnLBHr", - "function": { - "arguments": "{\"name\":\"Jason\",\"age\":25}", - "name": "UserDetail" - }, - "type": "function" - } - ] - } - } - ], - "created": 1708394134, - "model": "gpt-3.5-turbo-0125", - "object": "chat.completion", - "system_fingerprint": "fp_69829325d0", - "usage": { - "completion_tokens": 9, - "prompt_tokens": 81, - "total_tokens": 90 - } - } - """ - ``` +Notice that simply because we return the `create` method, the `extract()` function will return the correct user type. -## Why use Instructor? +![async](./blog/posts/img/async_type.png) -The question of using Instructor is fundamentally a question of why to use Pydantic. +### Returning the original completion: `create_with_completion` -1. **Powered by type hints** — Instructor is powered by Pydantic, which is powered by type hints. Schema validation, prompting is controlled by type annotations; less to learn, less code to write, and integrates with your IDE. +You can also return the original completion object + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +user, completion = client.chat.completions.create_with_completion( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) +``` + +![with_completion](./blog/posts/img/with_completion.png) + + +### Streaming Partial Objects: `create_partial` + +In order to handle streams, we still support `Iterable[T]` and `Partial[T]` but to simply the type inference, we've added `create_iterable` and `create_partial` methods as well! + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +user_stream = client.chat.completions.create_partial( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create a user"}, + ], + response_model=User, +) + +for user in user_stream: + print(user) + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=30 + #> name=None age=30 + #> name=None age=30 + #> name=None age=30 + #> name=None age=30 + #> name='John' age=30 + # name=None age=None + # name='' age=None + # name='John' age=None + # name='John Doe' age=None + # name='John Doe' age=30 +``` + +Notice now that the type infered is `Generator[User, None]` + +![generator](./blog/posts/img/generator.png) -2. **Powered by OpenAI** — Instructor is powered by OpenAI's function calling API. This means you can use the same API for both prompting and extraction. +### Streaming Iterables: `create_iterable` -3. **Customizable** — Pydantic is highly customizable. You can define your own validators, custom error messages, and more. +We get an iterable of objects when we want to extract multiple objects. + +```python +import openai +import instructor +from pydantic import BaseModel + + +client = instructor.from_openai(openai.OpenAI()) + + +class User(BaseModel): + name: str + age: int + + +users = client.chat.completions.create_iterable( + model="gpt-4-turbo-preview", + messages=[ + {"role": "user", "content": "Create 2 users"}, + ], + response_model=User, +) + +for user in users: + print(user) + #> name='Alice' age=30 + #> name='Bob' age=25 + # User(name='John Doe', age=30) + # User(name='Jane Smith', age=25) +``` -4. **Ecosystem** Pydantic is the most widely used data validation library for Python. It's used by FastAPI, Typer, and many other popular libraries. +![iterable](./blog/posts/img/iterable.png) -5. **Battle Tested** — Pydantic is downloaded over 100M times per month, and supported by a large community of contributors. +## Validation -6. **Easy Integration with CLI** - We offer a variety of CLI tools like `instructor jobs`, `instructor files` and `instructor usage` to track your OpenAI usage, fine-tuning jobs and more, just check out our [CLI Documentation](cli/index.md) to find out more. +You can also use Pydantic to validate your outputs and get the llm to retry on failure. Check out our docs on [retrying](./concepts/retrying.md) and [validation context](./concepts/reask_validation.md). ## More Examples diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 63e47e4af..526bb45f9 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,4 +1,10 @@ -{% extends "base.html" %} {% block announce %} For updates follow +{% extends "base.html" %} {% block announce %} + + +For updates follow @jxnlco on 7\u001b[0m name \u001b[39m=\u001b[39m obj\u001b[39m.\u001b[39mget(\u001b[39m\"\u001b[39m\u001b[39mfirst_name\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m age \u001b[39m=\u001b[39m obj\u001b[39m.\u001b[39mget(\u001b[39m\"\u001b[39m\u001b[39mage\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m----> 9\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39mf\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mNext year \u001b[39m\u001b[39m{\u001b[39;00mname\u001b[39m}\u001b[39;00m\u001b[39m will be \u001b[39m\u001b[39m{\u001b[39;00mage\u001b[39m+\u001b[39;49m\u001b[39m1\u001b[39;49m\u001b[39m}\u001b[39;00m\u001b[39m years old\u001b[39m\u001b[39m\"\u001b[39m)\n", - "\u001b[0;31mTypeError\u001b[0m: can only concatenate str (not \"int\") to str" + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/var/folders/l2/jjqj299126j0gycr9kkkt9xm0000gn/T/ipykernel_24047/2607506000.py\", line 10, in \n", + " age_next_year = age + 1\n", + " ~~~~^~~\n", + "TypeError: can only concatenate str (not \"int\") to str\n" ] } ], @@ -73,7 +92,11 @@ "for obj in data:\n", " name = obj.get(\"first_name\")\n", " age = obj.get(\"age\")\n", - " print(f\"Next year {name} will be {age+1} years old\")" + " try:\n", + " age_next_year = age + 1\n", + " print(f\"Next year {name} will be {age_next_year} years old\")\n", + " except TypeError:\n", + " traceback.print_exc()" ] }, { @@ -94,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -103,14 +126,13 @@ "Person(name='Sam', age=30)" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from pydantic import BaseModel, Field\n", - "\n", + "from pydantic import BaseModel, Field, ValidationError\n", "\n", "class Person(BaseModel):\n", " name: str\n", @@ -123,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -132,7 +154,7 @@ "Person(name='Sam', age=30)" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -145,48 +167,79 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ { - "ename": "AssertionError", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/jasonliu/dev/instructor/docs/tutorials/1-introduction.ipynb Cell 10\u001b[0m line \u001b[0;36m2\n\u001b[1;32m 1\u001b[0m \u001b[39massert\u001b[39;00m person\u001b[39m.\u001b[39mname \u001b[39m==\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mSam\u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m----> 2\u001b[0m \u001b[39massert\u001b[39;00m person\u001b[39m.\u001b[39mage \u001b[39m==\u001b[39m \u001b[39m20\u001b[39m\n", - "\u001b[0;31mAssertionError\u001b[0m: " + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/var/folders/l2/jjqj299126j0gycr9kkkt9xm0000gn/T/ipykernel_24047/3040264600.py\", line 5, in \n", + " assert person.age == 20\n", + " ^^^^^^^^^^^^^^^^\n", + "AssertionError\n" ] } ], "source": [ "assert person.name == \"Sam\"\n", - "assert person.age == 20" + "assert person.age == 30\n", + "\n", + "try:\n", + " assert person.age == 20\n", + "except AssertionError:\n", + " traceback.print_exc()" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { - "ename": "ValidationError", - "evalue": "2 validation errors for Person\nname\n Field required [type=missing, input_value={'first_name': 'Sam', 'age': '30.2'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.6/v/missing\nage\n Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='30.2', input_type=str]\n For further information visit https://errors.pydantic.dev/2.6/v/int_parsing", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValidationError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/jasonliu/dev/instructor/docs/tutorials/1-introduction.ipynb Cell 11\u001b[0m line \u001b[0;36m2\n\u001b[1;32m 1\u001b[0m \u001b[39m# Data is validated to get better error messages\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m person \u001b[39m=\u001b[39m Person\u001b[39m.\u001b[39;49mmodel_validate({\u001b[39m\"\u001b[39;49m\u001b[39mfirst_name\u001b[39;49m\u001b[39m\"\u001b[39;49m: \u001b[39m\"\u001b[39;49m\u001b[39mSam\u001b[39;49m\u001b[39m\"\u001b[39;49m, \u001b[39m\"\u001b[39;49m\u001b[39mage\u001b[39;49m\u001b[39m\"\u001b[39;49m: \u001b[39m\"\u001b[39;49m\u001b[39m30.2\u001b[39;49m\u001b[39m\"\u001b[39;49m})\n\u001b[1;32m 3\u001b[0m person\n", - "File \u001b[0;32m~/dev/instructor/.venv/lib/python3.11/site-packages/pydantic/main.py:509\u001b[0m, in \u001b[0;36mBaseModel.model_validate\u001b[0;34m(cls, obj, strict, from_attributes, context)\u001b[0m\n\u001b[1;32m 507\u001b[0m \u001b[39m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[1;32m 508\u001b[0m __tracebackhide__ \u001b[39m=\u001b[39m \u001b[39mTrue\u001b[39;00m\n\u001b[0;32m--> 509\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mcls\u001b[39;49m\u001b[39m.\u001b[39;49m__pydantic_validator__\u001b[39m.\u001b[39;49mvalidate_python(\n\u001b[1;32m 510\u001b[0m obj, strict\u001b[39m=\u001b[39;49mstrict, from_attributes\u001b[39m=\u001b[39;49mfrom_attributes, context\u001b[39m=\u001b[39;49mcontext\n\u001b[1;32m 511\u001b[0m )\n", - "\u001b[0;31mValidationError\u001b[0m: 2 validation errors for Person\nname\n Field required [type=missing, input_value={'first_name': 'Sam', 'age': '30.2'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.6/v/missing\nage\n Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='30.2', input_type=str]\n For further information visit https://errors.pydantic.dev/2.6/v/int_parsing" + "name": "stdout", + "output_type": "stream", + "text": [ + "Validation Error:\n", + "Field: name, Error: Field required\n", + "Field: age, Error: Input should be a valid integer, unable to parse string as an integer\n", + "\u001b[91m\n", + "Original Traceback Below\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/var/folders/l2/jjqj299126j0gycr9kkkt9xm0000gn/T/ipykernel_24047/621989455.py\", line 3, in \n", + " person = Person.model_validate({\"first_name\": \"Sam\", \"age\": \"30.2\"})\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/opt/homebrew/Caskroom/miniconda/base/envs/instructor/lib/python3.11/site-packages/pydantic/main.py\", line 509, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "pydantic_core._pydantic_core.ValidationError: 2 validation errors for Person\n", + "name\n", + " Field required [type=missing, input_value={'first_name': 'Sam', 'age': '30.2'}, input_type=dict]\n", + " For further information visit https://errors.pydantic.dev/2.6/v/missing\n", + "age\n", + " Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='30.2', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.6/v/int_parsing\n" ] } ], "source": [ "# Data is validated to get better error messages\n", - "person = Person.model_validate({\"first_name\": \"Sam\", \"age\": \"30.2\"})\n", - "person" + "try:\n", + " person = Person.model_validate({\"first_name\": \"Sam\", \"age\": \"30.2\"})\n", + "except ValidationError as e:\n", + " print(\"Validation Error:\")\n", + " for error in e.errors():\n", + " print(f\"Field: {error['loc'][0]}, Error: {error['msg']}\")\n", + "\n", + " print(f\"{RED}\\nOriginal Traceback Below{RESET}\")\n", + " traceback.print_exc()" ] }, { @@ -202,29 +255,78 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Fundamental problem with asking for JSON from OpenAI\n" + "## Fundamental problem with asking for JSON from OpenAI\n", + "\n", + "As we shall see below, the correct json format would be something of the format below:\n", + "\n", + "```python\n", + "{\n", + " \"name\": \"Jason\",\n", + " \"age\": 10\n", + "}\n", + "```\n", + "\n", + "However, we get errorenous outputs like:\n", + "\n", + "```python\n", + "{\n", + " \"jason\": 10\n", + "}\n", + "```" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "json that we want:\n", + "\n", + "{\n", + " \"name\": \"Jason\",\n", + " \"age\": 10\n", + "}\n", + "\n", + "error!!\n", + "{\n", + " \"jason\": 10\n", + "}\n", "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n", + "correctly parsed person=Person(name='jason', age=10)\n", + "error!!\n", + "{\n", + " \"Jason\": {\n", + " \"age\": 10\n", + " }\n", + "}\n", + "error!!\n", + "{\n", + " \"Jason\": {\n", + " \"age\": 10\n", + " }\n", + "}\n", + "error!!\n", + "{\n", + " \"Jason\": {\n", + " \"age\": 10\n", + " }\n", + "}\n", "error!!\n", - "{\"jason\": 10}\n", + "{\n", + " \"Jason\": {\n", + " \"age\": 10\n", + " }\n", + "}\n", "correctly parsed person=Person(name='Jason', age=10)\n", "correctly parsed person=Person(name='Jason', age=10)\n", - "correctly parsed person=Person(name='Jason', age=10)\n" + "error!!\n", + "{\n", + " \"jason\": 10\n", + "}\n" ] } ], @@ -242,6 +344,14 @@ " temperature=1,\n", ")\n", "\n", + "print(\"json that we want:\")\n", + "print(\"\"\"\n", + "{\n", + " \"name\": \"Jason\",\n", + " \"age\": 10\n", + "}\n", + "\"\"\")\n", + "\n", "for choice in resp.choices:\n", " json = choice.message.content\n", " try:\n", @@ -262,21 +372,25 @@ "\n", "**Function Calling**\n", "\n", - "In an API call, you can describe functions and have the model intelligently choose to output a JSON object containing arguments to call one or many functions. The Chat Completions API does not call the function; instead, the model generates JSON that you can use to call the function in your code.\n" + "In an API call, you can describe _functions_ and have the model intelligently\n", + "choose to output a _JSON object_ containing _arguments_ to call one or many\n", + "functions. The Chat Completions API does **not** call the function; instead, the\n", + "model generates _JSON_ that you can use to call the function in **your code**.\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "PersonBirthday(name='Jason Liu', age=30, birthday=datetime.date(2024, 2, 8))" + "PersonBirthday(name='Jason Liu', age=30, birthday=datetime.date(1994, 3, 26))" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -325,7 +439,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -339,7 +453,7 @@ " 'type': 'object'}" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -357,7 +471,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -380,7 +494,7 @@ " 'type': 'object'}" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -422,7 +536,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -431,7 +545,7 @@ "PersonAddress(name='Jason Liu', age=30, address=Address(address='123 Main St', city='San Francisco', state='CA'))" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -449,9 +563,9 @@ " {\n", " \"role\": \"user\",\n", " \"content\": f\"\"\"\n", - " Today is {datetime.date.today()} \n", + " Today is {datetime.date.today()}\n", "\n", - " Extract `Jason Liu is thirty years old his birthday is yesturday` \n", + " Extract `Jason Liu is thirty years old his birthday is yesturday`\n", " he lives at 123 Main St, San Francisco, CA\"\"\",\n", " },\n", " ],\n", @@ -504,7 +618,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/tutorials/3-0-applications-rag.ipynb b/docs/tutorials/3-0-applications-rag.ipynb index a1d30efaa..05e7b10da 100644 --- a/docs/tutorials/3-0-applications-rag.ipynb +++ b/docs/tutorials/3-0-applications-rag.ipynb @@ -94,7 +94,11 @@ "### Example 1) Improving Extractions\n", "\n", "One of the big limitations is that often times the query we embed and the text\n", - "A common method of using structured output is to extract information from a document and use it to answer a question. Directly, we can be creative in how we extract, summarize and generate potential questions in order for our embeddings to do better.\n", + "we are searching for may not have a direct match, leading to suboptimal results.\n", + "A common method of using structured output is to extract information from a\n", + "document and use it to answer a question. Directly, we can be creative in how we\n", + "extract, summarize and generate potential questions in order for our embeddings\n", + "to do better.\n", "\n", "For example, instead of using just a text chunk we could try to:\n", "\n", @@ -102,7 +106,8 @@ "2. extract hypothetical questions\n", "3. generate a summary of the text\n", "\n", - "In the example below, we use the `instructor` library to extract the key words and themes from a text chunk and use them to answer a question.\n" + "In the example below, we use the `instructor` library to extract the key words\n", + "and themes from a text chunk and use them to answer a question.\n" ] }, { @@ -129,14 +134,45 @@ "metadata": {}, "outputs": [ { - "ename": "NameError", - "evalue": "name 's' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/jasonliu/dev/instructor/docs/tutorials/3-0-applications-rag.ipynb Cell 10\u001b[0m line \u001b[0;36m3\n\u001b[1;32m 2\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mtyping\u001b[39;00m \u001b[39mimport\u001b[39;00m Iterable\n\u001b[1;32m 5\u001b[0m text_chunk \u001b[39m=\u001b[39m \u001b[39m\"\"\"\u001b[39m\n\u001b[1;32m 6\u001b[0m \u001b[39m## Simple RAG\u001b[39m\n\u001b[1;32m 7\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[39m - Issue: The model might provide general travel advice, ignoring the specific context of first-time travelers or European destinations.\u001b[39m\n\u001b[1;32m 26\u001b[0m \u001b[39m\"\"\"\u001b[39m\n\u001b[1;32m 28\u001b[0m extractions \u001b[39m=\u001b[39m client\u001b[39m.\u001b[39mchat\u001b[39m.\u001b[39mcompletions\u001b[39m.\u001b[39mcreate(\n\u001b[1;32m 29\u001b[0m model\u001b[39m=\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mgpt-4-1106-preview\u001b[39m\u001b[39m\"\u001b[39m,\n\u001b[1;32m 30\u001b[0m stream\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m,\n\u001b[0;32m---> 31\u001b[0m response_model\u001b[39m=\u001b[39ms,\n\u001b[1;32m 32\u001b[0m messages\u001b[39m=\u001b[39m[\n\u001b[1;32m 33\u001b[0m {\n\u001b[1;32m 34\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mrole\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39m\"\u001b[39m\u001b[39msystem\u001b[39m\u001b[39m\"\u001b[39m,\n\u001b[1;32m 35\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mcontent\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39m\"\u001b[39m\u001b[39mYour role is to extract chunks from the following and create a set of topics.\u001b[39m\u001b[39m\"\u001b[39m,\n\u001b[1;32m 36\u001b[0m },\n\u001b[1;32m 37\u001b[0m {\u001b[39m\"\u001b[39m\u001b[39mrole\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39m\"\u001b[39m\u001b[39muser\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39m\"\u001b[39m\u001b[39mcontent\u001b[39m\u001b[39m\"\u001b[39m: text_chunk},\n\u001b[1;32m 38\u001b[0m ],\n\u001b[1;32m 39\u001b[0m )\n\u001b[1;32m 42\u001b[0m \u001b[39mfor\u001b[39;00m extraction \u001b[39min\u001b[39;00m extractions:\n\u001b[1;32m 43\u001b[0m pprint(extraction\u001b[39m.\u001b[39mmodel_dump())\n", - "\u001b[0;31mNameError\u001b[0m: name 's' is not defined" + "name": "stdout", + "output_type": "stream", + "text": [ + "{'hypothetical_questions': ['What is the basic concept behind simple RAG?',\n", + " 'How does simple RAG work for information '\n", + " 'retrieval?'],\n", + " 'keywords': ['Simple RAG',\n", + " 'Retrieval-Augmented Generation',\n", + " 'user query',\n", + " 'embedding search',\n", + " 'vector database',\n", + " 'Wikipedia articles',\n", + " 'information retrieval'],\n", + " 'summary': 'The simplest implementation of Retrieval-Augmented Generation '\n", + " '(RAG) involves embedding a user query and conducting a single '\n", + " 'embedding search in a vector database, like a vector store of '\n", + " 'Wikipedia articles, to retrieve relevant information. This method '\n", + " 'may not be ideal for complex queries or varied data sources.',\n", + " 'topic': 'Simple RAG'}\n", + "{'hypothetical_questions': ['What are the drawbacks of using simple RAG '\n", + " 'systems?',\n", + " 'How does query-document mismatch affect the '\n", + " 'performance of RAG?',\n", + " 'Why is a monolithic search backend a limitation '\n", + " 'for RAG?'],\n", + " 'keywords': ['limitations',\n", + " 'query-document mismatch',\n", + " 'simple RAG',\n", + " 'monolithic search backend',\n", + " 'text search',\n", + " 'planning ability',\n", + " 'contextual information'],\n", + " 'summary': 'Key limitations of the simple RAG include query-document '\n", + " 'mismatch, reliance on a single search backend, constraints of '\n", + " 'text search capabilities, and limited planning ability to '\n", + " 'leverage contextual information. These issues can result in '\n", + " 'suboptimal search outcomes and retrieval of irrelevant or broad '\n", + " 'information.',\n", + " 'topic': 'Limitations of Simple RAG'}\n" ] } ], @@ -171,7 +207,7 @@ "extractions = client.chat.completions.create(\n", " model=\"gpt-4-1106-preview\",\n", " stream=True,\n", - " response_model=s,\n", + " response_model=Iterable[Extraction],\n", " messages=[\n", " {\n", " \"role\": \"system\",\n", @@ -190,7 +226,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now you can imagine if you were to embed the summaries, hypothetical questions, and keywords in a vector database, you can then use a vector search to find the best matching document for a given query. What you'll find is that the results are much better than if you were to just embed the text chunk!\n" + "Now you can imagine if you were to embed the summaries, hypothetical questions,\n", + "and keywords in a vector database (i.e. in the metadata fields of a vector\n", + "database), you can then use a vector search to find the best matching document\n", + "for a given query. What you'll find is that the results are much better than if\n", + "you were to just embed the text chunk!\n" ] }, { @@ -245,7 +285,7 @@ { "data": { "text/plain": [ - "Query(rewritten_query='recent developments in AI', published_daterange=DateRange(start=datetime.date(2023, 2, 9), end=datetime.date(2024, 2, 9)))" + "Query(rewritten_query='Recent developments in artificial intelligence', published_daterange=DateRange(start=datetime.date(2024, 1, 1), end=datetime.date(2024, 3, 31)))" ] }, "execution_count": 5, @@ -287,7 +327,7 @@ { "data": { "text/plain": [ - "Query(rewritten_query='Recent developments in Artificial Intelligence', published_daterange=DateRange(chain_of_thought=\"Considering 'recent' generally refers to the past few months or up to a year, the best date range to capture the most recent developments in AI would be from one year ago to today's date.\", start=datetime.date(2023, 2, 9), end=datetime.date(2024, 2, 9)))" + "Query(rewritten_query='latest advancements in artificial intelligence', published_daterange=DateRange(chain_of_thought='Since the user is asking for recent developments, it would be relevant to look for articles and papers published within the last year. Therefore, setting the start date to a year before today and the end date to today will cover the most recent advancements.', start=datetime.date(2023, 3, 31), end=datetime.date(2024, 3, 31)))" ] }, "execution_count": 6, @@ -353,25 +393,12 @@ "metadata": { "scrolled": true }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'helpers'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/jasonliu/dev/instructor/docs/tutorials/3-0-applications-rag.ipynb Cell 20\u001b[0m line \u001b[0;36m5\n\u001b[1;32m 2\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39minstructor\u001b[39;00m\n\u001b[1;32m 4\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mopenai\u001b[39;00m \u001b[39mimport\u001b[39;00m AsyncOpenAI\n\u001b[0;32m----> 5\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mhelpers\u001b[39;00m \u001b[39mimport\u001b[39;00m dicts_to_df\n\u001b[1;32m 6\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mdatetime\u001b[39;00m \u001b[39mimport\u001b[39;00m date\n\u001b[1;32m 7\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mpydantic\u001b[39;00m \u001b[39mimport\u001b[39;00m BaseModel, Field\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'helpers'" - ] - } - ], + "outputs": [], "source": [ "import json\n", "import instructor\n", "\n", "from openai import AsyncOpenAI\n", - "from helpers import dicts_to_df\n", "from datetime import date\n", "from pydantic import BaseModel, Field\n", "\n", @@ -423,157 +450,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "wandb version 0.16.1 is available! To upgrade, please run:\n", - " $ pip install wandb --upgrade" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Tracking run with wandb version 0.16.0" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Run data is saved locally in /Users/jasonliu/dev/instructor/tutorials/wandb/run-20231227_202003-7c9dxnfl" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Syncing run blooming-firefly-4 to Weights & Biases (docs)
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - " View project at https://wandb.ai/instructor/query" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - " View run at https://wandb.ai/instructor/query/runs/7c9dxnfl" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d73fb8a832254b32a938572fd27eca62", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(Label(value='0.011 MB of 0.011 MB uploaded (0.001 MB deduped)\\r'), FloatProgress(value=1.0, max…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "wandb: WARNING Source type is set to 'repo' but some required information is missing from the environment. A job will not be created from this run. See https://docs.wandb.ai/guides/launch/create-job\n" - ] - }, - { - "data": { - "text/html": [ - "W&B sync reduced upload amount by 6.6% " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - "

Run history:


average duration (s)
duration (s)
n_queries
usage_completion_tokens
usage_prompt_tokens
usage_total_tokens

Run summary:


average duration (s)2.28692
duration (s)9.14768
n_queries4
usage_completion_tokens359
usage_prompt_tokens780
usage_total_tokens1139

" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - " View run blooming-firefly-4 at: https://wandb.ai/instructor/query/runs/7c9dxnfl
Synced 4 W&B file(s), 2 media file(s), 5 artifact file(s) and 0 other file(s)" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Find logs at: ./wandb/run-20231227_202003-7c9dxnfl/logs" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], + "source": [ + "# % pip install pandas wandb\n", + "import pandas as pd\n", + "from typing import List, Dict, Any\n", + "\n", + "\n", + "def flatten_dict(d: Dict[str, Any], parent_key: str = \"\", sep: str = \"_\") -> Dict[str, Any]:\n", + " \"\"\"\n", + " Flatten a nested dictionary.\n", + "\n", + " :param d: The nested dictionary to flatten.\n", + " :param parent_key: The base key to use for the flattened keys.\n", + " :param sep: Separator to use between keys.\n", + " :return: A flattened dictionary.\n", + " \"\"\"\n", + " items = []\n", + " for k, v in d.items():\n", + " new_key = f\"{parent_key}{sep}{k}\" if parent_key else k\n", + " if isinstance(v, dict):\n", + " items.extend(flatten_dict(v, new_key, sep=sep).items())\n", + " else:\n", + " items.append((new_key, v))\n", + " return dict(items)\n", + "\n", + "\n", + "def dicts_to_df(list_of_dicts: List[Dict[str, Any]]) -> pd.DataFrame:\n", + " \"\"\"\n", + " Convert a list of dictionaries to a pandas DataFrame.\n", + "\n", + " :param list_of_dicts: List of dictionaries, potentially nested.\n", + " :return: A pandas DataFrame representing the flattened data.\n", + " \"\"\"\n", + " # Flatten each dictionary and create a DataFrame\n", + " flattened_data = [flatten_dict(d) for d in list_of_dicts]\n", + " return pd.DataFrame(flattened_data)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], "source": [ "import asyncio\n", "import time\n", @@ -614,7 +535,6 @@ "\n", "\n", "run.log({\"schema\": wandb.Table(dataframe=pd.DataFrame([{\"schema\": schema}]))})\n", - "\n", "run.log(\n", " {\n", " \"usage_total_tokens\": df[\"usage_total_tokens\"].sum(),\n", @@ -626,7 +546,6 @@ " }\n", ")\n", "\n", - "\n", "run.log(\n", " {\n", " \"results\": wandb.Table(dataframe=df),\n", @@ -634,16 +553,30 @@ ")\n", "\n", "files = wandb.Artifact(\"data\", type=\"dataset\")\n", - "\n", "files.add_file(\"schema.json\")\n", "files.add_file(\"results.jsonlines\")\n", "files.add_file(\"results.csv\")\n", "\n", - "\n", "run.log_artifact(files)\n", "run.finish()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output of Weights and Biases would return something like the below table.\n", + "\n", + "| Metric | Value |\n", + "|--------------------------|--------|\n", + "| average duration (s) | 1.5945 |\n", + "| duration (s) | 6.37799|\n", + "| n_queries | 4 |\n", + "| usage_completion_tokens | 376 |\n", + "| usage_prompt_tokens | 780 |\n", + "| usage_total_tokens | 1156 |\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -659,7 +592,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -691,7 +624,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -702,24 +635,30 @@ " \"queries\": [\n", " {\n", " \"query\": \"work\",\n", - " \"keywords\": [],\n", + " \"keywords\": [\n", + " \"work\",\n", + " \"today\"\n", + " ],\n", " \"email\": \"jason@work.com\",\n", - " \"source\": \"calendar\",\n", + " \"source\": \"gmail\",\n", " \"date_range\": {\n", - " \"chain_of_thought\": \"today\",\n", - " \"start\": \"2024-02-09\",\n", - " \"end\": \"2024-02-09\"\n", + " \"chain_of_thought\": \"Check today's work schedule\",\n", + " \"start\": \"2024-03-31\",\n", + " \"end\": \"2024-03-31\"\n", " }\n", " },\n", " {\n", - " \"query\": \"\",\n", - " \"keywords\": [],\n", + " \"query\": \"new emails\",\n", + " \"keywords\": [\n", + " \"email\",\n", + " \"new\"\n", + " ],\n", " \"email\": \"jason@work.com\",\n", " \"source\": \"gmail\",\n", " \"date_range\": {\n", - " \"chain_of_thought\": \"today\",\n", - " \"start\": \"2024-02-09\",\n", - " \"end\": \"2024-02-09\"\n", + " \"chain_of_thought\": \"Check for new emails today\",\n", + " \"start\": \"2024-03-31\",\n", + " \"end\": \"2024-03-31\"\n", " }\n", " }\n", " ]\n", @@ -735,7 +674,7 @@ " {\n", " \"role\": \"system\",\n", " \"content\": f\"\"\"You are Jason's personal assistant.\n", - " He has two emails jason@work.com jason@personal.com \n", + " He has two emails jason@work.com jason@personal.com\n", " Today is {date.today()}\"\"\",\n", " },\n", " {\"role\": \"user\", \"content\": \"What do I have today for work? any new emails?\"},\n", @@ -753,9 +692,36 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"queries\": [\n", + " {\n", + " \"query\": \"Jason's meetings\",\n", + " \"keywords\": [\n", + " \"meeting\",\n", + " \"appointment\",\n", + " \"schedule\",\n", + " \"calendar\"\n", + " ],\n", + " \"email\": \"jason@work.com\",\n", + " \"source\": \"calendar\",\n", + " \"date_range\": {\n", + " \"chain_of_thought\": \"Since today's date is 2024-03-31, we should look for meetings scheduled for this exact date.\",\n", + " \"start\": \"2024-03-31\",\n", + " \"end\": \"2024-03-31\"\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] + } + ], "source": [ "retrieval = client.chat.completions.create(\n", " model=\"gpt-4-1106-preview\",\n", @@ -764,7 +730,7 @@ " {\n", " \"role\": \"system\",\n", " \"content\": f\"\"\"You are Jason's personal assistant.\n", - " He has two emails jason@work.com jason@personal.com \n", + " He has two emails jason@work.com jason@personal.com\n", " Today is {date.today()}\"\"\",\n", " },\n", " {\n", @@ -798,7 +764,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -806,11 +772,11 @@ "output_type": "stream", "text": [ "{\n", - " \"root_question\": \"What is the difference between the population of jason's home country and canada?\",\n", + " \"root_question\": \"What is the difference between the population of Jason's home country and Canada?\",\n", " \"plan\": [\n", " {\n", " \"id\": 1,\n", - " \"query\": \"What is Jason's home country?\",\n", + " \"query\": \"What is the population of Jason's home country?\",\n", " \"subquestions\": []\n", " },\n", " {\n", @@ -820,17 +786,10 @@ " },\n", " {\n", " \"id\": 3,\n", - " \"query\": \"What is the population of {Jason's home country}?\",\n", - " \"subquestions\": [\n", - " 1\n", - " ]\n", - " },\n", - " {\n", - " \"id\": 4,\n", - " \"query\": \"What is the difference between the population of {Jason's home country} and the population of Canada?\",\n", + " \"query\": \"What is the difference between two population numbers?\",\n", " \"subquestions\": [\n", - " 2,\n", - " 3\n", + " 1,\n", + " 2\n", " ]\n", " }\n", " ]\n", @@ -897,7 +856,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/why.md b/docs/why.md index 2032bcd82..5a9bac1ae 100644 --- a/docs/why.md +++ b/docs/why.md @@ -1,9 +1,10 @@ # Why use Instructor? -??? question "Why use Pydantic?" +This is a letter from the author [Jason Liu](https://twitter.com/jxnlco) of Instructor. I'm a big fan of Pydantic and I think it's the best way to handle data validation in Python. I've been using it for years and I'm excited to bring it to the OpenAI API. - Its hard to answer the question of why use Instructor without first answering [why use Pydantic.](https://docs.pydantic.dev/latest/why/): +??? note "Why use Pydantic?" + Its hard to answer the question of why use Instructor without first answering [why use Pydantic.](https://docs.pydantic.dev/latest/why/): - **Powered by type hints** — with Pydantic, schema validation and serialization are controlled by type annotations; less to learn, less code to write, and integration with your IDE and static analysis tools. @@ -18,147 +19,233 @@ - **Battle tested** — Pydantic is downloaded over 70M times/month and is used by all FAANG companies and 20 of the 25 largest companies on NASDAQ. If you're trying to do something with Pydantic, someone else has probably already done it. -Our `instructor.patch` for the `OpenAI` class introduces three key enhancements: - -- **Response Mode:** Specify a Pydantic model to streamline data extraction. -- **Max Retries:** Set your desired number of retry attempts for requests. -- **Validation Context:** Provide a context object for enhanced validator access. - A Glimpse into Instructor's Capabilities +## No New standards -!!! note "Using Validators" +Instructor is built on top of Pydantic and OpenAI, which will be familiar to many developers already. But, since many llm providers support the OpenAI API spec, you can use many closed source and open source providers like Anyscale, Together, Groq, Ollama, and Llama-cpp-python. - Learn more about validators checkout our blog post [Good llm validation is just good validation](https://jxnl.github.io/instructor/blog/2023/10/23/good-llm-validation-is-just-good-validation/) +All we do is augment the `create` such that -With Instructor, your code becomes more efficient and readable. Here’s a quick peek: - -## Understanding the `patch` +```python +def create(response_model=Type[T]) -> T: +``` -Lets go over the `patch` function. And see how we can leverage it to make use of instructor +Check out how we connect with [open source](./blog/posts/open_source.md) + +## Pydantic over Raw Schema + +I find many prompt building tools to be overly complex and difficult to use, they might be simple to get started with a trivial examples but once you need more control, you have to wish they were simpler. Instructor does the least amount of work to get the job done. + +=== "Pydantic" + + Pydantic is more readable and definitions and reference values are handled automatically. This is a big win for Instructor, as it allows us to focus on the data extraction and not the schema. + + ```python + from typing import List, Literal + from pydantic import BaseModel, Field + + + class Property(BaseModel): + name: str = Field(description="name of property in snake case") + value: str + + class Character(BaseModel): + """ + Any character in a fictional story + """ + name: str + age: int + properties: List[Property] + role: Literal['protagonist', 'antagonist', 'supporting'] + + class AllCharacters(BaseModel): + characters: List[Character] = Field(description="A list of all characters in the story") + ``` + +=== "Json Schema" + + Would you Ever prefer to code review this? Where everything is a string, ripe for typos and errors in references? I know I wouldn't. + + ```python + var = { + "$defs": { + "Character": { + "description": "Any character in a fictional story", + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "properties": { + "type": "array", + "items": {"$ref": "#/$defs/Property"}, + "title": "Properties", + }, + "role": { + "enum": ["protagonist", "antagonist", "supporting"], + "title": "Role", + "type": "string", + }, + }, + "required": ["name", "age", "properties", "role"], + "title": "Character", + "type": "object", + }, + "Property": { + "properties": { + "name": { + "description": "name of property in snake case", + "title": "Name", + "type": "string", + }, + "value": {"title": "Value", "type": "string"}, + }, + "required": ["name", "value"], + "title": "Property", + "type": "object", + }, + }, + "properties": { + "characters": { + "description": "A list of all characters in the story", + "items": {"$ref": "#/$defs/Character"}, + "title": "Characters", + "type": "array", + } + }, + "required": ["characters"], + "title": "AllCharacters", + "type": "object", + } + ``` + +## Easy to try and install + +The minimum viable api just adds `response_model` to the client, if you dont think you want a model its very easy to remove it and continue building your application + +=== "Instructor" + + ```python + import instructor + from openai import OpenAI + from pydantic import BaseModel + + # Patch the OpenAI client with Instructor + client = instructor.from_openai(OpenAI()) + + class UserDetail(BaseModel): + name: str + age: int + + # Function to extract user details + def extract_user() -> UserDetail: + user = client.chat.completions.create( + model="gpt-4-turbo-preview", + response_model=UserDetail, + messages=[ + {"role": "user", "content": "Extract Jason is 25 years old"}, + ] + ) + return user + ``` + +=== "OpenAI" + + ```python + import openai + import json + + def extract_user() -> dict: + completion = client.chat.completions.create( + model="gpt-4-turbo-preview", + tools=[ + { + "type": "function", + "function": { + "name": "ExtractUser", + "description": "Correctly extracted `ExtractUser` with all the required parameters with correct types", + "parameters": { + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + "required": ["age", "name"], + "type": "object", + }, + }, + } + ], + tool_choice={"type": "function", "function": {"name": "ExtractUser"}}, + messages=[ + {"role": "user", "content": "Extract Jason is 25 years old"}, + ], + ) # type: ignore + + user = json_loads(completion.choices[0].message.tool_calls[0].function.arguments) + assert "name" in user, "Name is not in the response" + assert "age" in user, "Age is not in the response" + user["age"] = int(user["age"]) + return user + ``` -### Step 1: Patch the client +## Partial Extraction -First, import the required libraries and apply the `patch` function to the OpenAI module. This exposes new functionality with the `response_model` parameter. +We also support [partial](./concepts/partial.md) extraction, which is useful for streaming in data that is incomplete. ```python import instructor -from openai import OpenAI -# This enables response_model keyword -# from client.chat.completions.create -client = instructor.patch(OpenAI()) -``` - -### Step 2: Define the Pydantic Model - -Create a Pydantic model to define the structure of the data you want to extract. This model will map directly to the information in the prompt. - -```python +from instructor import Partial +from openai import OpenAI from pydantic import BaseModel +from typing import List +from rich.console import Console +client = instructor.from_openai(OpenAI()) -class UserDetail(BaseModel): +text_block = "..." + +class User(BaseModel): name: str - age: int -``` + email: str + twitter: str -### Step 3: Extract -Use the `client.chat.completions.create` method to send a prompt and extract the data into the Pydantic object. The `response_model` parameter specifies the Pydantic model to use for extraction. Its helpful to annotate the variable with the type of the response model, which will help your IDE provide autocomplete and spell check. +class MeetingInfo(BaseModel): + users: List[User] + date: str + location: str + budget: int + deadline: str -```python -user: UserDetail = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetail, + +extraction_stream = client.chat.completions.create( + model="gpt-4", + response_model=Partial[MeetingInfo], messages=[ - {"role": "user", "content": "Extract Jason is 25 years old"}, + { + "role": "user", + "content": f"Get the information about the meeting and the users {text_block}", + }, ], + stream=True, ) -assert user.name == "Jason" -assert user.age == 25 -``` - -## Understanding Validation - -Validation can also be plugged into the same Pydantic model. Here, if the answer attribute contains content that violates the rule "don't say objectionable things," Pydantic will raise a validation error. - -```python hl_lines="9 15" -from pydantic import BaseModel, ValidationError, BeforeValidator -from typing_extensions import Annotated -from instructor import llm_validator - - -class QuestionAnswer(BaseModel): - question: str - answer: Annotated[ - str, BeforeValidator(llm_validator("don't say objectionable things")) - ] +console = Console() -try: - qa = QuestionAnswer( - question="What is the meaning of life?", - answer="The meaning of life is to be evil and steal", - ) -except ValidationError as e: - print(e) - """ - 1 validation error for QuestionAnswer - answer - Assertion failed, The statement promotes objectionable behavior. [type=assertion_error, input_value='The meaning of life is to be evil and steal', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/assertion_error - """ -``` - -Its important to note here that the error message is generated by the LLM, not the code, so it'll be helpful for re-asking the model. - -```plaintext -1 validation error for QuestionAnswer -answer - Assertion failed, The statement is objectionable. (type=assertion_error) +for extraction in extraction_stream: + obj = extraction.model_dump() + console.clear() + console.print(obj) ``` -## Self Correcting on Validation Error - -Here, the `UserDetails` model is passed as the `response_model`, and `max_retries` is set to 2. - -```python -import instructor - -from openai import OpenAI -from pydantic import BaseModel, field_validator - -# Apply the patch to the OpenAI client -client = instructor.patch(OpenAI()) - - -class UserDetails(BaseModel): - name: str - age: int - - @field_validator("name") - @classmethod - def validate_name(cls, v): - if v.upper() != v: - raise ValueError("Name must be in uppercase.") - return v - +This will output the following: -model = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=UserDetails, - max_retries=2, - messages=[ - {"role": "user", "content": "Extract jason is 25 years old"}, - ], -) +![Partial Streaming Gif](./img/partial.gif) -assert model.name == "JASON" -``` +As you can see, we've baked in a self correcting mechanism into the model. This is a powerful way to make your models more robust and less brittle without including a lot of extra code or prompts. ## Iterables and Lists -We can also generate tasks as the tokens are streamed in by defining an `Iterable[T]` type. +We can also generate tasks as the tokens are streamed in by defining an [`Iterable[T]`](./concepts/lists.md) type. Lets look at an example in action with the same class @@ -197,73 +284,45 @@ for user in users: #> name="John" "age"=10 ``` -## Partial Extraction +## Simple Types + +We also support [simple types](./concepts/types.md), which are useful for extracting simple values like numbers, strings, and booleans. + +## Self Correcting on Validation Error -We also support partial extraction, which is useful for streaming in data that is incomplete. +Due to pydantic's very own validation model, easily add validators to the model to correct the data. +If we run this code, we will get a validation error because the name is not in uppercase. While we could have included a prompt to fix this, we can also just add a field validator to the model. This will result in two API calls, to make sure you do your best to prompt before adding validators. ```python import instructor -from instructor import Partial from openai import OpenAI -from pydantic import BaseModel -from typing import List -from rich.console import Console - -client = instructor.patch(OpenAI()) - -text_block = """ -In our recent online meeting, participants from various backgrounds joined to discuss the upcoming tech conference. The names and contact details of the participants were as follows: - -- Name: John Doe, Email: johndoe@email.com, Twitter: @TechGuru44 -- Name: Jane Smith, Email: janesmith@email.com, Twitter: @DigitalDiva88 -- Name: Alex Johnson, Email: alexj@email.com, Twitter: @CodeMaster2023 - -During the meeting, we agreed on several key points. The conference will be held on March 15th, 2024, at the Grand Tech Arena located at 4521 Innovation Drive. Dr. Emily Johnson, a renowned AI researcher, will be our keynote speaker. - -The budget for the event is set at $50,000, covering venue costs, speaker fees, and promotional activities. Each participant is expected to contribute an article to the conference blog by February 20th. +from pydantic import BaseModel, field_validator -A follow-up meetingis scheduled for January 25th at 3 PM GMT to finalize the agenda and confirm the list of speakers. -""" +# Apply the patch to the OpenAI client +client = instructor.from_openai(OpenAI()) -class User(BaseModel): +class UserDetails(BaseModel): name: str - email: str - twitter: str - + age: int -class MeetingInfo(BaseModel): - users: List[User] - date: str - location: str - budget: int - deadline: str + @field_validator("name") + @classmethod + def validate_name(cls, v): + if v.upper() != v: + raise ValueError("Name must be in uppercase.") + return v -extraction_stream = client.chat.completions.create( - model="gpt-4", - response_model=Partial[MeetingInfo], +model = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetails, + max_retries=2, messages=[ - { - "role": "user", - "content": f"Get the information about the meeting and the users {text_block}", - }, + {"role": "user", "content": "Extract jason is 25 years old"}, ], - stream=True, ) - -console = Console() - -for extraction in extraction_stream: - obj = extraction.model_dump() - console.clear() - console.print(obj) +assert model.name == "JASON" ``` - -This will output the following: - -![Partial Streaming Gif](./img/partial.gif) - -As you can see, we've baked in a self correcting mechanism into the model. This is a powerful way to make your models more robust and less brittle without including a lot of extra code or prompts. diff --git a/examples/anthropic/run.py b/examples/anthropic/run.py new file mode 100644 index 000000000..f3f09e12f --- /dev/null +++ b/examples/anthropic/run.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel +import anthropic +import instructor + +# Patching the Anthropics client with the instructor for enhanced capabilities +client = instructor.from_anthropic(anthropic.Anthropic()) + + +class Properties(BaseModel): + key: str + value: str + + +class User(BaseModel): + name: str + age: int + properties: list[Properties] + + +user = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name, age, and properties.", + } + ], + response_model=User, +) + +print(user.model_dump_json(indent=2)) diff --git a/examples/auto-ticketer/run.py b/examples/auto-ticketer/run.py index d242d48b5..5002ba45b 100644 --- a/examples/auto-ticketer/run.py +++ b/examples/auto-ticketer/run.py @@ -1,11 +1,11 @@ import instructor from openai import OpenAI -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field from enum import Enum -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class PriorityEnum(str, Enum): @@ -32,11 +32,11 @@ class Ticket(BaseModel): name: str = Field(..., description="Title of the task") description: str = Field(..., description="Detailed description of the task") priority: PriorityEnum = Field(..., description="Priority level") - assignees: List[str] = Field(..., description="List of users assigned to the task") - subtasks: Optional[List[Subtask]] = Field( + assignees: list[str] = Field(..., description="List of users assigned to the task") + subtasks: Optional[list[Subtask]] = Field( None, description="List of subtasks associated with the main task" ) - dependencies: Optional[List[int]] = Field( + dependencies: Optional[list[int]] = Field( None, description="List of ticket IDs that this ticket depends on" ) @@ -46,10 +46,10 @@ class ActionItems(BaseModel): Correctly resolved set of action items from the given transcript """ - items: List[Ticket] + items: list[Ticket] -def generate(data: str) -> ActionItems: +def generate(data: str): return client.chat.completions.create( model="gpt-3.5-turbo-0613", response_model=ActionItems, @@ -63,7 +63,7 @@ def generate(data: str) -> ActionItems: "content": f"Create the action items for the following transcript: {data}", }, ], - ) # type: ignore + ) prediction = generate( diff --git a/examples/avail/run.py b/examples/avail/run.py index 5ed30aa3e..fd99111a0 100644 --- a/examples/avail/run.py +++ b/examples/avail/run.py @@ -1,11 +1,12 @@ from pydantic import BaseModel, Field -from typing import Iterable, List, Literal +from typing import Literal +from collections.abc import Iterable from datetime import datetime, timedelta from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class DateRange(BaseModel): @@ -17,7 +18,7 @@ class DateRange(BaseModel): default=None, description="If the date range repeats, and how often, this way we can generalize the date range to the future., if its special, then we can assume it is a one time event.", ) - days_of_week: List[ + days_of_week: list[ Literal[ "monday", "tuesday", @@ -41,7 +42,7 @@ class DateRange(BaseModel): class AvailabilityResponse(BaseModel): - availability: List[DateRange] + availability: list[DateRange] def prepare_dates(n=7) -> str: diff --git a/examples/avail/run_mixtral.py b/examples/avail/run_mixtral.py index 4fc84e9d4..aa88f0324 100644 --- a/examples/avail/run_mixtral.py +++ b/examples/avail/run_mixtral.py @@ -1,19 +1,19 @@ import os from pydantic import BaseModel, Field -from typing import Iterable, List, Literal +from typing import Literal from datetime import datetime, timedelta from openai import OpenAI import instructor -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="https://api.endpoints.anyscale.com/v1", api_key=os.environ["ANYSCALE_API_KEY"], ), mode=instructor.Mode.JSON_SCHEMA, + model="mistralai/Mixtral-8x7B-Instruct-v0.1", ) -model = "mistralai/Mixtral-8x7B-Instruct-v0.1" class DateRange(BaseModel): @@ -25,7 +25,7 @@ class DateRange(BaseModel): default=None, description="If the date range repeats, and how often, this way we can generalize the date range to the future., if its special, then we can assume it is a one time event.", ) - days_of_week: List[ + days_of_week: list[ Literal[ "monday", "tuesday", @@ -49,7 +49,7 @@ class DateRange(BaseModel): class AvailabilityResponse(BaseModel): - availability: List[DateRange] + availability: list[DateRange] def prepare_dates(n=7) -> str: @@ -67,9 +67,8 @@ def prepare_dates(n=7) -> str: return acc.strip() -def parse_availability(text: str) -> Iterable[AvailabilityResponse]: - return client.chat.completions.create( - model=model, +def parse_availability(text: str): + return client.chat.completions.create_iterable( max_tokens=10000, messages=[ { @@ -85,7 +84,7 @@ def parse_availability(text: str) -> Iterable[AvailabilityResponse]: "content": f"To help you understand the dates, here are the next 7 days: {prepare_dates()}", }, ], - response_model=Iterable[AvailabilityResponse], + response_model=AvailabilityResponse, max_retries=3, ) diff --git a/examples/batch-classification/run-cache.py b/examples/batch-classification/run-cache.py index 774a8f4d8..c9f349cd9 100644 --- a/examples/batch-classification/run-cache.py +++ b/examples/batch-classification/run-cache.py @@ -3,11 +3,9 @@ from openai import AsyncOpenAI from pydantic import BaseModel, Field, field_validator -from typing import List from enum import Enum -client = AsyncOpenAI() -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(AsyncOpenAI(), mode=instructor.Mode.TOOLS) sem = asyncio.Semaphore(5) @@ -41,7 +39,7 @@ class QuestionClassification(BaseModel): chain_of_thought: str = Field( ..., description="The chain of thought that led to the classification" ) - classification: List[QuestionType] = Field( + classification: list[QuestionType] = Field( description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used", ) @@ -54,7 +52,7 @@ def validate_classification(cls, v): # Modify the classify function -async def classify(data: str) -> QuestionClassification: +async def classify(data: str): async with sem: # some simple rate limiting return data, await client.chat.completions.create( model="gpt-4", @@ -69,7 +67,7 @@ async def classify(data: str) -> QuestionClassification: ) -async def main(questions: List[str]): +async def main(questions: list[str]): tasks = [classify(question) for question in questions] resps = [] for task in asyncio.as_completed(tasks): diff --git a/examples/batch-classification/run.py b/examples/batch-classification/run.py index 3a48c1fa5..dc8223ec4 100644 --- a/examples/batch-classification/run.py +++ b/examples/batch-classification/run.py @@ -4,11 +4,10 @@ from openai import AsyncOpenAI from pydantic import BaseModel, Field, field_validator -from typing import List from enum import Enum client = AsyncOpenAI() -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) sem = asyncio.Semaphore(5) @@ -42,7 +41,7 @@ class QuestionClassification(BaseModel): chain_of_thought: str = Field( ..., description="The chain of thought that led to the classification" ) - classification: List[QuestionType] = Field( + classification: list[QuestionType] = Field( description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used", ) @@ -54,7 +53,7 @@ def validate_classification(cls, v): return v -async def classify(data: str) -> QuestionClassification: +async def classify(data: str): async with sem: # some simple rate limiting return data, await client.chat.completions.create( model="gpt-4", @@ -69,9 +68,7 @@ async def classify(data: str) -> QuestionClassification: ) -async def main( - questions: List[str], *, path_to_jsonl: str = None -) -> List[QuestionClassification]: +async def main(questions: list[str], *, path_to_jsonl: str = None): tasks = [classify(question) for question in questions] for task in asyncio.as_completed(tasks): question, label = await task diff --git a/examples/batch-classification/run_langsmith.py b/examples/batch-classification/run_langsmith.py index 030e249b1..144bab3cf 100644 --- a/examples/batch-classification/run_langsmith.py +++ b/examples/batch-classification/run_langsmith.py @@ -6,11 +6,10 @@ from openai import AsyncOpenAI from pydantic import BaseModel, Field, field_validator -from typing import List from enum import Enum client = wrap_openai(AsyncOpenAI()) -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) sem = asyncio.Semaphore(5) @@ -44,7 +43,7 @@ class QuestionClassification(BaseModel): chain_of_thought: str = Field( ..., description="The chain of thought that led to the classification" ) - classification: List[QuestionType] = Field( + classification: list[QuestionType] = Field( description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used", ) @@ -58,7 +57,7 @@ def validate_classification(cls, v): # Modify the classify function @traceable(name="classify-question") -async def classify(data: str) -> QuestionClassification: +async def classify(data: str): async with sem: # some simple rate limiting return data, await client.chat.completions.create( model="gpt-4", @@ -73,7 +72,7 @@ async def classify(data: str) -> QuestionClassification: ) -async def main(questions: List[str]): +async def main(questions: list[str]): tasks = [classify(question) for question in questions] resps = [] for task in asyncio.as_completed(tasks): diff --git a/examples/caching/example_diskcache.py b/examples/caching/example_diskcache.py index 1217341c0..aceb1767a 100644 --- a/examples/caching/example_diskcache.py +++ b/examples/caching/example_diskcache.py @@ -3,10 +3,11 @@ import instructor import diskcache -from openai import OpenAI +from openai import OpenAI, AsyncOpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) +aclient = instructor.from_openai(AsyncOpenAI()) class UserDetail(BaseModel): @@ -23,6 +24,8 @@ def instructor_cache(func): if not issubclass(return_type, BaseModel): raise ValueError("The return type must be a Pydantic model") + is_async = inspect.iscoroutinefunction(func) + @functools.wraps(func) def wrapper(*args, **kwargs): key = f"{func.__name__}-{functools._make_key(args, kwargs, typed=False)}" @@ -39,7 +42,23 @@ def wrapper(*args, **kwargs): return result - return wrapper + @functools.wraps(func) + async def awrapper(*args, **kwargs): + key = f"{func.__name__}-{functools._make_key(args, kwargs, typed=False)}" + # Check if the result is already cached + if (cached := cache.get(key)) is not None: + # Deserialize from JSON based on the return type + if issubclass(return_type, BaseModel): + return return_type.model_validate_json(cached) + + # Call the function and cache its result + result = await func(*args, **kwargs) + serialized_result = result.model_dump_json() + cache.set(key, serialized_result) + + return result + + return wrapper if not is_async else awrapper @instructor_cache @@ -50,7 +69,18 @@ def extract(data) -> UserDetail: messages=[ {"role": "user", "content": data}, ], - ) + ) # type: ignore + + +@instructor_cache +async def aextract(data) -> UserDetail: + return await aclient.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": data}, + ], + ) # type: ignore def test_extract(): @@ -69,7 +99,27 @@ def test_extract(): print(f"Time taken: {time.perf_counter() - start}") +async def atest_extract(): + import time + + start = time.perf_counter() + model = await aextract("Extract jason is 25 years old") + assert model.name.lower() == "jason" + assert model.age == 25 + print(f"Time taken: {time.perf_counter() - start}") + + start = time.perf_counter() + model = await aextract("Extract jason is 25 years old") + assert model.name.lower() == "jason" + assert model.age == 25 + print(f"Time taken: {time.perf_counter() - start}") + + if __name__ == "__main__": test_extract() # Time taken: 0.7285366660216823 # Time taken: 9.841693099588156e-05 + + import asyncio + + asyncio.run(atest_extract()) diff --git a/examples/caching/example_redis.py b/examples/caching/example_redis.py index cdc1242ad..27bb3fe9a 100644 --- a/examples/caching/example_redis.py +++ b/examples/caching/example_redis.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) cache = redis.Redis("localhost") diff --git a/examples/caching/lru.py b/examples/caching/lru.py index c4e7352c0..97f26afe2 100644 --- a/examples/caching/lru.py +++ b/examples/caching/lru.py @@ -3,7 +3,7 @@ from pydantic import BaseModel import functools -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserDetail(BaseModel): diff --git a/examples/chain-of-density/chain_of_density.py b/examples/chain-of-density/chain_of_density.py index a0d73ed9e..862edf200 100644 --- a/examples/chain-of-density/chain_of_density.py +++ b/examples/chain-of-density/chain_of_density.py @@ -1,11 +1,10 @@ from pydantic import BaseModel, Field, field_validator -from typing import List import instructor import nltk from openai import OpenAI import spacy -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) nlp = spacy.load("en_core_web_sm") @@ -38,12 +37,12 @@ class RewrittenSummary(BaseModel): ..., description="This is a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities. It should have the same length ( ~ 80 words ) as the previous summary and should be easily understood without the Article", ) - absent: List[str] = Field( + absent: list[str] = Field( ..., default_factory=list, description="this is a list of Entities found absent from the new summary that were present in the previous summary", ) - missing: List[str] = Field( + missing: list[str] = Field( default_factory=list, description="This is a list of 1-3 informative Entities from the Article that are missing from the new summary which should be included in the next generated summary.", ) @@ -77,7 +76,7 @@ def min_length(cls, v: str): return v @field_validator("missing") - def has_missing_entities(cls, missing_entities: List[str]): + def has_missing_entities(cls, missing_entities: list[str]): if len(missing_entities) == 0: raise ValueError( "You must identify 1-3 informative Entities from the Article which are missing from the previously generated summary to be used in a new summary" @@ -85,7 +84,7 @@ def has_missing_entities(cls, missing_entities: List[str]): return missing_entities @field_validator("absent") - def has_no_absent_entities(cls, absent_entities: List[str]): + def has_no_absent_entities(cls, absent_entities: list[str]): absent_entity_string = ",".join(absent_entities) if len(absent_entities) > 0: print(f"Detected absent entities of {absent_entity_string}") diff --git a/examples/chain-of-density/finetune.py b/examples/chain-of-density/finetune.py index 17ba8fecb..d9d621ef2 100644 --- a/examples/chain-of-density/finetune.py +++ b/examples/chain-of-density/finetune.py @@ -1,4 +1,3 @@ -from typing import List from openai import OpenAI from chain_of_density import summarize_article import csv @@ -8,7 +7,7 @@ logging.basicConfig(level=logging.INFO) -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) instructions = instructor.Instructions( name="Chain Of Density", @@ -41,11 +40,11 @@ class GeneratedSummary(BaseModel): @instructions.distil def distil_summarization(text: str) -> GeneratedSummary: - summary_chain: List[str] = summarize_article(text) + summary_chain: list[str] = summarize_article(text) return GeneratedSummary(summary=summary_chain[-1]) -with open("test.csv", "r") as file: +with open("test.csv") as file: reader = csv.reader(file) next(reader) # Skip the header for article, _summary in reader: diff --git a/examples/citation_with_extraction/citation_fuzzy_match.py b/examples/citation_with_extraction/citation_fuzzy_match.py index 925d6aebc..195cca4f4 100644 --- a/examples/citation_with_extraction/citation_fuzzy_match.py +++ b/examples/citation_with_extraction/citation_fuzzy_match.py @@ -1,18 +1,17 @@ import instructor -from typing import List from loguru import logger from openai import OpenAI from pydantic import Field, BaseModel, FieldValidationInfo, model_validator -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Fact(BaseModel): statement: str = Field( ..., description="Body of the sentence, as part of a response" ) - substring_phrase: List[str] = Field( + substring_phrase: list[str] = Field( ..., description="String quote long enough to evaluate the truthfulness of the fact", ) @@ -65,7 +64,7 @@ class QuestionAnswer(instructor.OpenAISchema): each sentence contains a body and a list of sources.""" question: str = Field(..., description="Question that was asked") - answer: List[Fact] = Field( + answer: list[Fact] = Field( ..., description="Body of the answer, each fact should be its seperate object with a body and a list of sources", ) @@ -82,29 +81,19 @@ def validate_sources(self) -> "QuestionAnswer": def ask_ai(question: str, context: str) -> QuestionAnswer: - completion = client.chat.completions.create( + return client.chat.completions.create( model="gpt-3.5-turbo-0613", temperature=0, - functions=[QuestionAnswer.openai_schema], - function_call={"name": QuestionAnswer.openai_schema["name"]}, + response_model=QuestionAnswer, messages=[ { "role": "system", - "content": "You are a world class algorithm to answer questions with correct and exact citations. ", + "content": "You are a world class algorithm to answer questions with correct and exact citations.", }, - {"role": "user", "content": "Answer question using the following context"}, {"role": "user", "content": f"{context}"}, {"role": "user", "content": f"Question: {question}"}, - { - "role": "user", - "content": "Tips: Make sure to cite your sources, and use the exact words from the context.", - }, ], - ) - - # Creating an Answer object from the completion response - return QuestionAnswer.from_response( - completion, validation_context={"text_chunk": context} + validation_context={"text_chunk": context}, ) diff --git a/examples/citation_with_extraction/main.py b/examples/citation_with_extraction/main.py index 9cae6b6d8..58af2c971 100644 --- a/examples/citation_with_extraction/main.py +++ b/examples/citation_with_extraction/main.py @@ -1,5 +1,5 @@ import json -from typing import Iterable, List +from collections.abc import Iterable from fastapi import FastAPI, Request, HTTPException from fastapi.params import Depends from instructor import OpenAISchema @@ -13,7 +13,7 @@ from openai import OpenAI from instructor.dsl.multitask import MultiTaskBase -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) logger = logging.getLogger(__name__) # FastAPI app @@ -33,7 +33,7 @@ class Fact(BaseModel): ..., description="Body of the sentences, as part of a response, it should read like a sentence that answers the question", ) - substring_quotes: List[str] = Field( + substring_quotes: list[str] = Field( ..., description="Each source should be a direct quote from the context, as a substring of the original content", ) @@ -65,7 +65,7 @@ class QuestionAnswer(OpenAISchema, MultiTaskBase): each sentence contains a body and a list of sources.""" question: str = Field(..., description="Question that was asked") - tasks: List[Fact] = Field( + tasks: list[Fact] = Field( ..., description="Body of the answer, each fact should be its separate object with a body and a list of sources", ) diff --git a/examples/citations/run.py b/examples/citations/run.py index c8af6994e..a47bd18d8 100644 --- a/examples/citations/run.py +++ b/examples/citations/run.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from openai import OpenAI from pydantic import ( BaseModel, @@ -11,7 +11,7 @@ import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) """ Example 1) Simple Substring check that compares a citation to a text chunk @@ -39,7 +39,7 @@ def substring_quote_exists(cls, v: str, info: ValidationInfo): class AnswerWithCitaton(BaseModel): question: str - answer: List[Statements] + answer: list[Statements] try: @@ -111,7 +111,7 @@ def substring_quote_exists(self, info: ValidationInfo): class AnswerWithCitaton(BaseModel): question: str - answer: List[Statements] + answer: list[Statements] resp = AnswerWithCitaton.model_validate( @@ -169,7 +169,7 @@ class AnswerWithCitaton(BaseModel): # that also verifies that the citations are aligned with the answers class AnswerWithCitaton(BaseModel): question: str - answer: List[Statements] + answer: list[Statements] @model_validator(mode="after") def validate_answer(self, info: ValidationInfo): diff --git a/examples/classification/classifiy_with_validation.py b/examples/classification/classifiy_with_validation.py new file mode 100644 index 000000000..29f2a293c --- /dev/null +++ b/examples/classification/classifiy_with_validation.py @@ -0,0 +1,176 @@ +# pip install openai instructor +from pydantic import BaseModel, field_validator, Field +import openai +import instructor +from tqdm import tqdm + +client = instructor.from_openai(openai.OpenAI()) + +classes = { + "11-0000": "Management", + "13-0000": "Business and Financial Operations", + "15-0000": "Computer and Mathematical", + "17-0000": "Architecture and Engineering", + "19-0000": "Life, Physical, and Social Science", + "21-0000": "Community and Social Service", + "23-0000": "Legal", + "25-0000": "Education Instruction and Library", + "27-0000": "Arts, Design, Entertainment, Sports and Media", + "29-0000": "Healthcare Practitioners and Technical", + "31-0000": "Healthcare Support", + "33-0000": "Protective Service", + "35-0000": "Food Preparation and Serving", + "37-0000": "Building and Grounds Cleaning and Maintenance", + "39-0000": "Personal Care and Service", + "41-0000": "Sales and Related", + "43-0000": "Office and Administrative Support", + "45-0000": "Farming, Fishing and Forestry", + "47-0000": "Construction and Extraction", + "49-0000": "Installation, Maintenance, and Repair", + "51-0000": "Production Occupations", + "53-0000": "Transportation and Material Moving", + "55-0000": "Military Specific", + "99-0000": "Other", +} + + +class SOCCode(BaseModel): + reasoning: str = Field( + default=None, + description="Step-by-step reasoning to get the correct classification", + ) + code: str + + @field_validator("code") + def validate_code(cls, v): + if v not in classes: + raise ValueError(f"Invalid SOC code, {v}") + return v + + +def classify_job(description: str) -> SOCCode: + response = client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=SOCCode, + max_retries=3, + messages=[ + { + "role": "system", + "content": f"You are an expert at classifying job descriptions into Standard Occupational Classification (SOC) codes. from the following list: {classes}", + }, + { + "role": "user", + "content": f"Classify this job description into the most appropriate SOC code: {description}", + }, + ], + ) + return response + + +if __name__ == "__main__": + # gpt-3.5-turbo: 16/20 + # gpt-3.5-turbo (COT): 18/20 + # gpt-4-turbo: 20/20 + + job_descriptions = [ + ( + "Develop and design complex software applications for various industries, including finance, healthcare, and e-commerce", + "15-0000", # Computer and Mathematical Occupations + ), + ( + "Provide comprehensive technical support and troubleshooting for enterprise-level software products, ensuring seamless user experience", + "15-0000", # Computer and Mathematical Occupations + ), + ( + "Teach a diverse range of subjects to elementary school students, fostering their intellectual and social development", + "25-0000", # Education, Training, and Library Occupations + ), + ( + "Conduct cutting-edge research in various academic fields at a renowned university, contributing to the advancement of knowledge", + "25-0000", # Education, Training, and Library Occupations + ), + ( + "Design visually appealing and strategically effective logos, branding, and marketing materials for clients across different industries", + "27-0000", # Arts, Design, Entertainment, Sports, and Media Occupations + ), + ( + "Perform as part of a professional musical group, entertaining audiences and showcasing artistic talent", + "27-0000", # Arts, Design, Entertainment, Sports, and Media Occupations + ), + ( + "Diagnose and treat a wide range of injuries and medical conditions, providing comprehensive healthcare services to patients", + "29-0000", # Healthcare Practitioners and Technical Occupations + ), + ( + "Assist doctors and nurses in delivering high-quality patient care, ensuring the smooth operation of healthcare facilities", + "31-0000", # Healthcare Support Occupations + ), + ( + "Patrol assigned areas to enforce laws and ordinances, maintaining public safety and order in the community", + "33-0000", # Protective Service Occupations + ), + ( + "Prepare and serve a diverse menu of delectable meals in a fast-paced restaurant environment", + "35-0000", # Food Preparation and Serving Related Occupations + ), + ( + "Maintain the cleanliness and upkeep of various buildings and facilities, ensuring a safe and presentable environment", + "37-0000", # Building and Grounds Cleaning and Maintenance Occupations + ), + ( + "Provide a range of beauty services, such as haircuts, styling, and manicures, to help clients look and feel their best", + "39-0000", # Personal Care and Service Occupations + ), + ( + "Engage with customers in a retail setting, providing excellent service and assisting them in finding the products they need", + "41-0000", # Sales and Related Occupations + ), + ( + "Perform a variety of clerical duties in an office environment, supporting the overall operations of the organization", + "43-0000", # Office and Administrative Support Occupations + ), + ( + "Cultivate and harvest a wide range of crops, contributing to the production of food and other agricultural products", + "45-0000", # Farming, Fishing, and Forestry Occupations + ), + ( + "Construct and build various structures, including residential, commercial, and infrastructure projects", + "47-0000", # Construction and Extraction Occupations + ), + ( + "Repair and maintain a diverse range of mechanical equipment, ensuring their proper functioning and longevity", + "49-0000", # Installation, Maintenance, and Repair Occupations + ), + ( + "Operate specialized machinery and equipment in a manufacturing setting to produce high-quality goods", + "51-0000", # Production Occupations + ), + ( + "Transport freight and goods across different regions, ensuring timely and efficient delivery", + "53-0000", # Transportation and Material Moving Occupations + ), + ( + "Serve in the armed forces, protecting the nation and its citizens through various military operations and duties", + "55-0000", # Military Specific Occupations + ), + ] + + correct = 0 + errors = [] + for description, expected_code in tqdm(job_descriptions): + try: + predicted_code = None + result = classify_job(description) + predicted_code = result.code + assert ( + result.code == expected_code + ), f"Expected {expected_code}, got {result.code} for description: {description}" + correct += 1 + except Exception as e: + errors.append( + f"Got {classes.get(predicted_code, 'Unknown')} expected {classes.get(expected_code, 'Unknown')}" + ) + + print(f"{correct} out of {len(job_descriptions)} tests passed!") + for error in errors: + print(error) diff --git a/examples/classification/multi_prediction.py b/examples/classification/multi_prediction.py index 127833f57..27d51a911 100644 --- a/examples/classification/multi_prediction.py +++ b/examples/classification/multi_prediction.py @@ -1,11 +1,10 @@ import enum import instructor -from typing import List from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) # Define new Enum class for multiple labels @@ -17,7 +16,7 @@ class MultiLabels(str, enum.Enum): # Adjust the prediction model to accommodate a list of labels class MultiClassPrediction(BaseModel): - predicted_labels: List[MultiLabels] + predicted_labels: list[MultiLabels] # Modify the classify function diff --git a/examples/classification/simple_prediction.py b/examples/classification/simple_prediction.py index 60bb180f1..cf67bd395 100644 --- a/examples/classification/simple_prediction.py +++ b/examples/classification/simple_prediction.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Labels(str, enum.Enum): diff --git a/examples/codegen-from-schema/create_fastapi_app.py b/examples/codegen-from-schema/create_fastapi_app.py index b3ec85c35..56884829e 100644 --- a/examples/codegen-from-schema/create_fastapi_app.py +++ b/examples/codegen-from-schema/create_fastapi_app.py @@ -20,7 +20,7 @@ import openai import instructor -instructor.patch() +instructor.from_openai() app = FastAPI() diff --git a/examples/codegen-from-schema/models.py b/examples/codegen-from-schema/models.py index 3f9a4e637..0bf7d1e0a 100644 --- a/examples/codegen-from-schema/models.py +++ b/examples/codegen-from-schema/models.py @@ -5,7 +5,6 @@ from __future__ import annotations from enum import Enum -from typing import List from pydantic import BaseModel @@ -24,4 +23,4 @@ class PhoneNumber(BaseModel): class ExtractPerson(BaseModel): name: str age: int - phoneNumbers: List[PhoneNumber] + phoneNumbers: list[PhoneNumber] diff --git a/examples/cohere/cohere.py b/examples/cohere/cohere.py new file mode 100644 index 000000000..d3aff0ec9 --- /dev/null +++ b/examples/cohere/cohere.py @@ -0,0 +1,59 @@ +import cohere +import instructor +from pydantic import BaseModel, Field + + +# Patching the Cohere client with the instructor for enhanced capabilities +client = instructor.from_cohere( + cohere.Client(), + max_tokens=1000, + model="command-r-plus", +) + + +class Person(BaseModel): + name: str = Field(description="name of the person") + country_of_origin: str = Field(description="country of origin of the person") + + +class Group(BaseModel): + group_name: str = Field(description="name of the group") + members: list[Person] = Field(description="list of members in the group") + + +task = """\ +Given the following text, create a Group object for 'The Beatles' band + +Text: +The Beatles were an English rock band formed in Liverpool in 1960. With a line-up comprising John Lennon, Paul McCartney, George Harrison and Ringo Starr, they are regarded as the most influential band of all time. The group were integral to the development of 1960s counterculture and popular music's recognition as an art form. +""" +group = client.messages.create( + response_model=Group, + messages=[{"role": "user", "content": task}], + temperature=0, +) + +print(group.model_dump_json(indent=2)) +""" +{ + "group_name": "The Beatles", + "members": [ + { + "name": "John Lennon", + "country_of_origin": "England" + }, + { + "name": "Paul McCartney", + "country_of_origin": "England" + }, + { + "name": "George Harrison", + "country_of_origin": "England" + }, + { + "name": "Ringo Starr", + "country_of_origin": "England" + } + ] +} +""" diff --git a/examples/crm/run.py b/examples/crm/run.py index 9bc83db84..db931b923 100644 --- a/examples/crm/run.py +++ b/examples/crm/run.py @@ -1,10 +1,9 @@ -from typing import List from enum import Enum from pydantic import BaseModel, Field import instructor from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class CRMSource(Enum): @@ -36,7 +35,7 @@ class CRMSearchQuery(BaseModel): for large locations decompose into multiple queries of smaller locations """ - queries: List[CRMSearch] + queries: list[CRMSearch] def query_crm(query: str) -> CRMSearchQuery: diff --git a/examples/distilations/three_digit_mul_dispatch.py b/examples/distilations/three_digit_mul_dispatch.py index 1feb611ad..0c91a1b41 100644 --- a/examples/distilations/three_digit_mul_dispatch.py +++ b/examples/distilations/three_digit_mul_dispatch.py @@ -4,7 +4,7 @@ from instructor import Instructions import instructor -instructor.patch() +instructor.from_openai() logging.basicConfig(level=logging.INFO) diff --git a/examples/evals/eval.py b/examples/evals/eval.py index d4b04c1de..999c64b15 100644 --- a/examples/evals/eval.py +++ b/examples/evals/eval.py @@ -1,6 +1,6 @@ from collections import Counter, defaultdict from enum import Enum -from typing import Any, Dict, Union +from typing import Any, Union import numpy as np import json from pydantic import ValidationError @@ -62,7 +62,7 @@ def update(self, index, data: Any, path: str = "$") -> None: else: self.accumulator[path].update(index, data) - def summarize(self) -> Dict[str, Dict]: + def summarize(self) -> dict[str, dict]: return {k: v.summarize(key_name=k) for k, v in self.accumulator.items()} @@ -105,7 +105,7 @@ def update(self, index: Any, value: Any) -> None: self.str_sum_length += str_len self.str_squared_sum_length += str_len**2 - def summarize(self, key_name=None) -> Dict[str, Union[int, float, dict]]: + def summarize(self, key_name=None) -> dict[str, Union[int, float, dict]]: if key_name is None: key_name = "" n = sum(self.counter.values()) diff --git a/examples/evals/models.py b/examples/evals/models.py index e406b9270..326bdedaf 100644 --- a/examples/evals/models.py +++ b/examples/evals/models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field from enum import Enum @@ -16,9 +16,9 @@ class Search(BaseModel): source_type: SourceType results_limit: Optional[int] = Field(10) is_priority: Optional[bool] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None class MultiSearch(BaseModel): - queries: List[Search] + queries: list[Search] user_id: Optional[str] diff --git a/examples/evals/streamlit.py b/examples/evals/streamlit.py index 92e9f2b07..3b5fcc694 100644 --- a/examples/evals/streamlit.py +++ b/examples/evals/streamlit.py @@ -2,7 +2,7 @@ from stats_dict import stats_dict # Sample data -query_data = {i: line.strip() for i, line in enumerate(open("test.jsonl", "r"))} +query_data = {i: line.strip() for i, line in enumerate(open("test.jsonl"))} # Initialize selected keys selected_keys = {} diff --git a/examples/extract-table/run_text.py b/examples/extract-table/run_text.py deleted file mode 100644 index 84a28118a..000000000 --- a/examples/extract-table/run_text.py +++ /dev/null @@ -1,92 +0,0 @@ -from openai import OpenAI -from io import StringIO -from typing import Annotated, Any, Iterable -from pydantic import ( - BaseModel, - BeforeValidator, - PlainSerializer, - InstanceOf, - WithJsonSchema, -) -import pandas as pd -import instructor - - -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) - - -def md_to_df(data: Any) -> Any: - if isinstance(data, str): - return ( - pd.read_csv( - StringIO(data), # Get rid of whitespaces - sep="|", - index_col=1, - ) - .dropna(axis=1, how="all") - .iloc[1:] - .map(lambda x: x.strip()) - ) - return data - - -MarkdownDataFrame = Annotated[ - InstanceOf[pd.DataFrame], - BeforeValidator(md_to_df), - PlainSerializer(lambda x: x.to_markdown()), - WithJsonSchema( - { - "type": "string", - "description": """ - The markdown representation of the table, - each one should be tidy, do not try to join tables - that should be seperate""", - } - ), -] - - -class Table(BaseModel): - caption: str - dataframe: MarkdownDataFrame - - -client = instructor.patch(OpenAI()) - - -tables = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=Iterable[Table], - messages=[ - { - "role": "system", - "content": "Please extract the tables from the following text, merge as much as possible:", - }, - { - "role": "user", - "content": """ - My name is John and I am 25 years old. I live in - New York and I like to play basketball. His name is - Mike and he is 30 years old. He lives in San Francisco - and he likes to play baseball. Sarah is 20 years old - and she lives in Los Angeles. She likes to play tennis. - Her name is Mary and she is 35 years old. - She lives in Chicago. - """, - }, - ], -) - -for table in tables: - print(table.caption) - print(table.dataframe) - print() - """ - People - Age City Hobby - Name - John 25 New York Basketball - Mike 30 San Francisco Baseball - Sarah 20 Los Angeles Tennis - Mary 35 Chicago N/A - """ diff --git a/examples/extract-table/run_vision.py b/examples/extract-table/run_vision.py index 4bcbd88c8..5994a8ca5 100644 --- a/examples/extract-table/run_vision.py +++ b/examples/extract-table/run_vision.py @@ -1,6 +1,6 @@ from openai import OpenAI from io import StringIO -from typing import Annotated, Any, List +from typing import Annotated, Any from pydantic import ( BaseModel, BeforeValidator, @@ -10,10 +10,13 @@ ) import instructor import pandas as pd +from rich.console import Console - -client = OpenAI() -client = instructor.patch(client, mode=instructor.function_calls.Mode.MD_JSON) +console = Console() +client = instructor.from_openai( + client=OpenAI(), + mode=instructor.Mode.TOOLS, +) def md_to_df(data: Any) -> Any: @@ -27,7 +30,7 @@ def md_to_df(data: Any) -> Any: .dropna(axis=1, how="all") .iloc[1:] .map(lambda x: x.strip()) - ) + ) # type: ignore return data @@ -53,7 +56,7 @@ class Table(BaseModel): class MultipleTables(BaseModel): - tables: List[Table] + tables: list[Table] example = MultipleTables( @@ -73,18 +76,14 @@ class MultipleTables(BaseModel): def extract(url: str) -> MultipleTables: - tables = client.chat.completions.create( - model="gpt-4-vision-preview", + return client.chat.completions.create( + model="gpt-4-turbo", max_tokens=4000, response_model=MultipleTables, messages=[ { "role": "user", "content": [ - { - "type": "text", - "text": f"Describe this data accurately as a table in markdown format. {example.model_dump_json(indent=2)}", - }, { "type": "image_url", "image_url": {"url": url}, @@ -92,11 +91,10 @@ def extract(url: str) -> MultipleTables: { "type": "text", "text": """ - First take a moment to reason about the best set of headers for the tables. - Write a good h1 for the image above. Then follow up with a short description of the what the data is about. - Then for each table you identified, write a h2 tag that is a descriptive title of the table. - Then follow up with a short description of the what the data is about. - Lastly, produce the markdown table for each table you identified. + First, analyze the image to determine the most appropriate headers for the tables. + Generate a descriptive h1 for the overall image, followed by a brief summary of the data it contains. + For each identified table, create an informative h2 title and a concise description of its contents. + Finally, output the markdown representation of each table. Make sure to escape the markdown table properly, and make sure to include the caption and the dataframe. @@ -107,7 +105,6 @@ def extract(url: str) -> MultipleTables: } ], ) - return tables.model_dump() urls = [ @@ -115,7 +112,41 @@ def extract(url: str) -> MultipleTables: "https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png/m/2880x0", ] - for url in urls: - tables = extract(url) - print(tables) + for table in extract(url).tables: + console.print(table.caption, "\n", table.dataframe) +""" +Growth in app installations and sessions across different app categories in Q3 2022 compared to Q2 2022 for Ireland and U.K. + Install Growth (%) Session Growth (%) + Category +Education 7 6 +Games 13 3 +Social 4 -3 +Utilities 6 -0.4 +Top 10 Grossing Android Apps in Ireland, October 2023 + App Name Category + Rank +1 Google One Productivity +2 Disney+ Entertainment +3 TikTok - Videos, Music & LIVE Entertainment +4 Candy Crush Saga Games +5 Tinder: Dating, Chat & Friends Social networking +6 Coin Master Games +7 Roblox Games +8 Bumble - Dating & Make Friends Dating +9 Royal Match Games +10 Spotify: Music and Podcasts Music & Audio +Top 10 Grossing iOS Apps in Ireland, October 2023 + App Name Category + Rank +1 Tinder: Dating, Chat & Friends Social networking +2 Disney+ Entertainment +3 YouTube: Watch, Listen, Stream Entertainment +4 Audible: Audio Entertainment Entertainment +5 Candy Crush Saga Games +6 TikTok - Videos, Music & LIVE Entertainment +7 Bumble - Dating & Make Friends Dating +8 Roblox Games +9 LinkedIn: Job Search & News Business +10 Duolingo - Language Lessons Education +""" diff --git a/examples/extract-table/run_vision_langsmith.py b/examples/extract-table/run_vision_langsmith.py index 65141938c..17ec99878 100644 --- a/examples/extract-table/run_vision_langsmith.py +++ b/examples/extract-table/run_vision_langsmith.py @@ -1,6 +1,6 @@ from openai import OpenAI from io import StringIO -from typing import Annotated, Any, List +from typing import Annotated, Any from pydantic import ( BaseModel, BeforeValidator, @@ -15,7 +15,7 @@ client = wrap_openai(OpenAI()) -client = instructor.patch(client, mode=instructor.function_calls.Mode.MD_JSON) +client = instructor.from_openai(client, mode=instructor.function_calls.Mode.MD_JSON) def md_to_df(data: Any) -> Any: @@ -55,7 +55,7 @@ class Table(BaseModel): class MultipleTables(BaseModel): - tables: List[Table] + tables: list[Table] example = MultipleTables( diff --git a/examples/extract-table/run_vision_org.py b/examples/extract-table/run_vision_org.py new file mode 100644 index 000000000..53c3e0bfd --- /dev/null +++ b/examples/extract-table/run_vision_org.py @@ -0,0 +1,89 @@ +from openai import OpenAI +from pydantic import BaseModel, Field +from rich.console import Console + +import instructor + +console = Console() +client = instructor.from_openai( + client=OpenAI(), + mode=instructor.Mode.TOOLS, +) + + +class People(BaseModel): + id: str + name: str + role: str + reports: list[str] = Field( + default_factory=list, description="People who report to this person" + ) + manages: list[str] = Field( + default_factory=list, description="People who this person manages" + ) + + +class Organization(BaseModel): + people: list[People] + + +def extract(url: str): + return client.chat.completions.create_partial( + model="gpt-4-turbo", + max_tokens=4000, + response_model=Organization, + messages=[ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": url}, + }, + { + "type": "text", + "text": """ + Analyze the organizational chart image and extract the relevant information to reconstruct the hierarchy. + + Create a list of People objects, where each person has the following attributes: + - id: A unique identifier for the person + - name: The person's name + - role: The person's role or position in the organization + - reports: A list of IDs of people who report directly to this person + - manages: A list of IDs of people who this person manages + + Ensure that the relationships between people are accurately captured in the reports and manages attributes. + + Return the list of People objects as the people attribute of an Organization object. + """, + }, + ], + } + ], + ) + + +console.print( + extract( + "https://www.mindmanager.com/static/mm/images/features/org-chart/hierarchical-chart.png" + ) +) +""" +Organization( + people=[ + People(id='A1', name='Adele Morana', role='Founder, Chairman & CEO', reports=[], manages=['B1', 'C1', 'D1']), + People(id='B1', name='Winston Cole', role='COO', reports=['A1'], manages=['E1']), + People(id='C1', name='Marcus Kim', role='CFO', reports=['A1'], manages=['F1']), + People(id='D1', name='Karin Ludovicicus', role='CPO', reports=['A1'], manages=['G1']), + People(id='E1', name='Lea Erastos', role='Chief Business Officer', reports=['B1'], manages=['H1', 'I1']), + People(id='F1', name='John McKinley', role='Chief Accounting Officer', reports=['C1'], manages=[]), + People(id='G1', name='Ayda Williams', role='VP, Global Customer & Business Marketing', reports=['D1'], manages=['J1', 'K1']), + People(id='H1', name='Zahida Mahtab', role='VP, Global Affairs & Communication', reports=['E1'], manages=[]), + People(id='I1', name='Adelaide Zhu', role='VP, Central Services', reports=['E1'], manages=[]), + People(id='J1', name='Gabriel Drummond', role='VP, Investor Relations', reports=['G1'], manages=[]), + People(id='K1', name='Nicholas Brambilla', role='VP, Company Brand', reports=['G1'], manages=[]), + People(id='L1', name='Felice Vasili', role='VP Finance', reports=['C1'], manages=[]), + People(id='M1', name='Sandra Herminius', role='VP, Product Marketing', reports=['D1'], manages=[]) + ] +) +""" diff --git a/examples/extract-table/run_vision_org_table.py b/examples/extract-table/run_vision_org_table.py new file mode 100644 index 000000000..c0c85a09d --- /dev/null +++ b/examples/extract-table/run_vision_org_table.py @@ -0,0 +1,115 @@ +from openai import OpenAI +from io import StringIO +from typing import Annotated, Any +from pydantic import ( + BaseModel, + BeforeValidator, + PlainSerializer, + InstanceOf, + WithJsonSchema, +) +import instructor +import pandas as pd +from rich.console import Console + +console = Console() +client = instructor.from_openai( + client=OpenAI(), + mode=instructor.Mode.TOOLS, +) + + +def md_to_df(data: Any) -> Any: + if isinstance(data, str): + return ( + pd.read_csv( + StringIO(data), # Get rid of whitespaces + sep="|", + index_col=1, + ) + .dropna(axis=1, how="all") + .iloc[1:] + .map(lambda x: x.strip()) + ) # type: ignore + return data + + +MarkdownDataFrame = Annotated[ + InstanceOf[pd.DataFrame], + BeforeValidator(md_to_df), + PlainSerializer(lambda x: x.to_markdown()), + WithJsonSchema( + { + "type": "string", + "description": """ + The markdown representation of the table, + each one should be tidy, do not try to join tables + that should be seperate""", + } + ), +] + + +class Table(BaseModel): + caption: str + dataframe: MarkdownDataFrame + + +def extract(url: str): + return client.chat.completions.create( + model="gpt-4-turbo", + max_tokens=4000, + response_model=Table, + messages=[ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": url}, + }, + { + "type": "text", + "text": """ + Analyze the organizational chart image and extract the relevant information to reconstruct the hierarchy. + + Create a list of People objects, where each person has the following attributes: + - id: A unique identifier for the person + - name: The person's name + - role: The person's role or position in the organization + - manager_name: The name of the person who manages this person + - manager_role: The role of the person who manages this person + + Ensure that the relationships between people are accurately captured in the reports and manages attributes. + + Return the list of People objects as the people attribute of an Organization object. + """, + }, + ], + } + ], + ) + + +print( + extract( + "https://www.mindmanager.com/static/mm/images/features/org-chart/hierarchical-chart.png" + ).model_dump()["dataframe"] +) +""" +| id | name | role | manager_name | manager_role | +|-------:|:-------------------|:-----------------------------------------|:------------------|:-----------------------------| +| 1 | Adele Morana | Founder, Chairman & CEO | | | +| 2 | Winston Cole | COO | Adele Morana | Founder, Chairman & CEO | +| 3 | Marcus Kim | CFO | Adele Morana | Founder, Chairman & CEO | +| 4 | Karin Ludovicus | CPO | Adele Morana | Founder, Chairman & CEO | +| 5 | Lea Erastos | Chief Business Officer | Winston Cole | COO | +| 6 | John McKinley | Chief Accounting Officer | Winston Cole | COO | +| 7 | Zahida Mahtab | VP, Global Affairs & Communication | Winston Cole | COO | +| 8 | Adelaide Zhu | VP, Central Services | Winston Cole | COO | +| 9 | Gabriel Drummond | VP, Investor Relations | Marcus Kim | CFO | +| 10 | Felicie Vasili | VP, Finance | Marcus Kim | CFO | +| 11 | Ayda Williams | VP, Global Customer & Business Marketing | Karin Ludovicius | CPO | +| 12 | Nicholas Brambilla | VP, Company Brand | Karin Ludovicius | CPO | +| 13 | Sandra Herminius | VP, Product Marketing | Karin Ludovicius | CPO | +""" diff --git a/examples/extract-table/test.py b/examples/extract-table/test.py new file mode 100644 index 000000000..3ec708cf5 --- /dev/null +++ b/examples/extract-table/test.py @@ -0,0 +1,106 @@ +from pydantic import BaseModel + +from openai import OpenAI +import instructor + +client = OpenAI() + +client = instructor.from_openai(client) + + +class User(BaseModel): + name: str + email: str + + +class MeetingInfo(BaseModel): + user: User + date: str + location: str + budget: int + deadline: str + + +data = """ +Jason Liu jason@gmail.com +Meeting Date: 2024-01-01 +Meeting Location: 1234 Main St +Meeting Budget: $1000 +Meeting Deadline: 2024-01-31 +""" +stream1 = client.chat.completions.create_partial( + model="gpt-4", + response_model=MeetingInfo, + messages=[ + { + "role": "user", + "content": f"Get the information about the meeting and the users {data}", + }, + ], + stream=True, +) # type: ignore + +for message in stream1: + print(message) +""" +ser={} date=None location=None budget=None deadline=None +user={} date=None location=None budget=None deadline=None +user={} date=None location=None budget=None deadline=None +user={} date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=100 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline='2024-01-31' +user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline='2024-01-31' +""" diff --git a/examples/extracting-pii/run.py b/examples/extracting-pii/run.py index 8d269ef80..3a880ffb4 100644 --- a/examples/extracting-pii/run.py +++ b/examples/extracting-pii/run.py @@ -1,10 +1,9 @@ -from typing import List from pydantic import BaseModel import instructor from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Data(BaseModel): @@ -18,7 +17,7 @@ class PIIDataExtraction(BaseModel): Extracted PII data from a document, all data_types should try to have consistent property names """ - private_data: List[Data] + private_data: list[Data] def scrub_data(self, content): """ diff --git a/examples/fizzbuzz/run.py b/examples/fizzbuzz/run.py index 0b3cde2c2..c92ea52f5 100644 --- a/examples/fizzbuzz/run.py +++ b/examples/fizzbuzz/run.py @@ -1,14 +1,15 @@ -from typing import List +from __future__ import annotations + from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) -def fizzbuzz_gpt(n) -> List[int | str]: +def fizzbuzz_gpt(n) -> list[int | str]: return client.chat.completions.create( model="gpt-3.5-turbo", - response_model=List[int | str], + response_model=list[int | str], messages=[ { "role": "user", diff --git a/examples/gpt-engineer/generate.py b/examples/gpt-engineer/generate.py index a9f5efcfc..963438d62 100644 --- a/examples/gpt-engineer/generate.py +++ b/examples/gpt-engineer/generate.py @@ -1,11 +1,10 @@ import instructor from openai import OpenAI -from typing import List from pydantic import Field from instructor import OpenAISchema -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class File(OpenAISchema): @@ -28,7 +27,7 @@ class Program(OpenAISchema): Set of files that represent a complete and correct program """ - files: List[File] = Field(..., description="List of files") + files: list[File] = Field(..., description="List of files") def develop(data: str) -> Program: diff --git a/examples/gpt-engineer/refactor.py b/examples/gpt-engineer/refactor.py index 389387ddf..e9d506a82 100644 --- a/examples/gpt-engineer/refactor.py +++ b/examples/gpt-engineer/refactor.py @@ -5,7 +5,7 @@ from instructor import OpenAISchema from generate import Program -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Diff(OpenAISchema): diff --git a/examples/groq/groq_example.py b/examples/groq/groq_example.py new file mode 100644 index 000000000..fdab07d03 --- /dev/null +++ b/examples/groq/groq_example.py @@ -0,0 +1,44 @@ +import os +from pydantic import BaseModel, Field +from groq import Groq +import instructor + + +class Character(BaseModel): + name: str + fact: list[str] = Field(..., description="A list of facts about the subject") + + +client = Groq( + api_key=os.environ.get("GROQ_API_KEY"), +) + +client = instructor.from_groq(client, mode=instructor.Mode.TOOLS) + +resp = client.chat.completions.create( + model="mixtral-8x7b-32768", + messages=[ + { + "role": "user", + "content": "Tell me about the company Tesla", + } + ], + response_model=Character, +) +print(resp.model_dump_json(indent=2)) +""" +{ + "name": "Tesla", + "fact": [ + "An American electric vehicle and clean energy company.", + "Co-founded by Elon Musk, JB Straubel, Martin Eberhard, Marc Tarpenning, and Ian Wright in 2003.", + "Headquartered in Austin, Texas.", + "Produces electric vehicles, energy storage solutions, and more recently, solar energy products.", + "Known for its premium electric vehicles, such as the Model S, Model 3, Model X, and Model Y.", + "One of the world's most valuable car manufacturers by market capitalization.", + "Tesla's CEO, Elon Musk, is also the CEO of SpaceX, Neuralink, and The Boring Company.", + "Tesla operates the world's largest global network of electric vehicle supercharging stations.", + "The company aims to accelerate the world's transition to sustainable transport and energy through innovative technologies and products." + ] +} +""" diff --git a/examples/groq/groq_example2.py b/examples/groq/groq_example2.py new file mode 100644 index 000000000..15b5d3817 --- /dev/null +++ b/examples/groq/groq_example2.py @@ -0,0 +1,36 @@ +import os +from pydantic import BaseModel +from groq import Groq +import instructor + +client = Groq( + api_key=os.environ.get("GROQ_API_KEY"), +) + +client = instructor.from_groq(client, mode=instructor.Mode.TOOLS) + + +class UserExtract(BaseModel): + name: str + age: int + + +user: UserExtract = client.chat.completions.create( + model="mixtral-8x7b-32768", + response_model=UserExtract, + messages=[ + {"role": "user", "content": "Extract jason is 25 years old"}, + ], +) + +assert isinstance(user, UserExtract), "Should be instance of UserExtract" +assert user.name.lower() == "jason" +assert user.age == 25 + +print(user.model_dump_json(indent=2)) +""" +{ + "name": "jason", + "age": 25 +} +""" diff --git a/examples/knowledge-graph/run.py b/examples/knowledge-graph/run.py index fb96d27b7..ec42298e5 100644 --- a/examples/knowledge-graph/run.py +++ b/examples/knowledge-graph/run.py @@ -2,11 +2,10 @@ from graphviz import Digraph from pydantic import BaseModel, Field -from typing import List from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Node(BaseModel): @@ -23,8 +22,8 @@ class Edge(BaseModel): class KnowledgeGraph(BaseModel): - nodes: List[Node] = Field(..., default_factory=list) - edges: List[Edge] = Field(..., default_factory=list) + nodes: list[Node] = Field(..., default_factory=list) + edges: list[Edge] = Field(..., default_factory=list) def generate_graph(input) -> KnowledgeGraph: diff --git a/examples/knowledge-graph/run_stream.py b/examples/knowledge-graph/run_stream.py index 074692fd2..f0899fcce 100644 --- a/examples/knowledge-graph/run_stream.py +++ b/examples/knowledge-graph/run_stream.py @@ -2,11 +2,11 @@ import instructor from graphviz import Digraph -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Node(BaseModel): @@ -29,8 +29,8 @@ def __hash__(self) -> int: class KnowledgeGraph(BaseModel): - nodes: Optional[List[Node]] = Field(..., default_factory=list) - edges: Optional[List[Edge]] = Field(..., default_factory=list) + nodes: Optional[list[Node]] = Field(..., default_factory=list) + edges: Optional[list[Edge]] = Field(..., default_factory=list) def update(self, other: "KnowledgeGraph") -> "KnowledgeGraph": """Updates the current graph with the other graph, deduplicating nodes and edges.""" @@ -54,7 +54,7 @@ def draw(self, prefix: str = None): dot.render(prefix, format="png", view=True) -def generate_graph(input: List[str]) -> KnowledgeGraph: +def generate_graph(input: list[str]) -> KnowledgeGraph: cur_state = KnowledgeGraph() num_iterations = len(input) for i, inp in enumerate(input): diff --git a/examples/logfire-fastapi/Readme.md b/examples/logfire-fastapi/Readme.md new file mode 100644 index 000000000..83a5dcd29 --- /dev/null +++ b/examples/logfire-fastapi/Readme.md @@ -0,0 +1,11 @@ +# Instructions + +1. Create a virtual environment and install all of the packages inside `requirements.txt` + +2. Run the server using + +``` +uvicorn server:app --reload +``` + +3. Open up the documentation at `http://127.0.0.1:8000/docs` to start experimenting with fastapi! You can print out the streaming example using `test.py`. diff --git a/examples/logfire-fastapi/requirements.txt b/examples/logfire-fastapi/requirements.txt new file mode 100644 index 000000000..0c15d3ca7 --- /dev/null +++ b/examples/logfire-fastapi/requirements.txt @@ -0,0 +1,7 @@ +pydantic==2.7.1 +openai==1.24.1 +instructor==1.0.3 +logfire==0.28.0 +fastapi==0.110.3 +uvicorn[standard] +logfire[fastapi] \ No newline at end of file diff --git a/examples/logfire-fastapi/server.py b/examples/logfire-fastapi/server.py new file mode 100644 index 000000000..3daae6ff0 --- /dev/null +++ b/examples/logfire-fastapi/server.py @@ -0,0 +1,84 @@ +from pydantic import BaseModel +from fastapi import FastAPI +from openai import AsyncOpenAI +import instructor +import logfire +import asyncio +from collections.abc import Iterable +from fastapi.responses import StreamingResponse + + +class UserData(BaseModel): + query: str + + +class MultipleUserData(BaseModel): + queries: list[str] + + +class UserDetail(BaseModel): + name: str + age: int + + +app = FastAPI() +openai_client = AsyncOpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_fastapi(app) +logfire.instrument_openai(openai_client) +client = instructor.from_openai(openai_client) + + +@app.post("/user", response_model=UserDetail) +async def endpoint_function(data: UserData) -> UserDetail: + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{data.query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + +@app.post("/many-users", response_model=list[UserDetail]) +async def extract_many_users(data: MultipleUserData): + async def extract_user(query: str): + user_detail = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=UserDetail, + messages=[ + {"role": "user", "content": f"Extract: `{query}`"}, + ], + ) + logfire.info("/User returning", value=user_detail) + return user_detail + + coros = [extract_user(query) for query in data.queries] + return await asyncio.gather(*coros) + + +@app.post("/extract", response_class=StreamingResponse) +async def extract(data: UserData): + supressed_client = AsyncOpenAI() + logfire.instrument_openai(supressed_client, suppress_other_instrumentation=False) + client = instructor.from_openai(supressed_client) + users = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=Iterable[UserDetail], + stream=True, + messages=[ + {"role": "user", "content": data.query}, + ], + ) + + async def generate(): + with logfire.span("Generating User Response Objects"): + async for user in users: + resp_json = user.model_dump_json() + logfire.info("Returning user object", value=resp_json) + + yield resp_json + + return StreamingResponse(generate(), media_type="text/event-stream") diff --git a/examples/logfire-fastapi/test.py b/examples/logfire-fastapi/test.py new file mode 100644 index 000000000..499b4da95 --- /dev/null +++ b/examples/logfire-fastapi/test.py @@ -0,0 +1,13 @@ +import requests + +response = requests.post( + "http://127.0.0.1:3000/extract", + json={ + "query": "Alice and Bob are best friends. They are currently 32 and 43 respectively. " + }, + stream=True, +) + +for chunk in response.iter_content(chunk_size=1024): + if chunk: + print(str(chunk, encoding="utf-8"), end="\n") diff --git a/examples/logfire/classify.py b/examples/logfire/classify.py new file mode 100644 index 000000000..47680fcb8 --- /dev/null +++ b/examples/logfire/classify.py @@ -0,0 +1,52 @@ +import enum +from pydantic import BaseModel +from openai import OpenAI +import instructor +import logfire + + +class Labels(str, enum.Enum): + """Enumeration for single-label text classification.""" + + SPAM = "spam" + NOT_SPAM = "not_spam" + + +class SinglePrediction(BaseModel): + """ + Class for a single class label prediction. + """ + + class_label: Labels + + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +client = instructor.from_openai(openai_client) + + +@logfire.instrument("classification", extract_args=True) +def classify(data: str) -> SinglePrediction: + """Perform single-label classification on the input text.""" + return client.chat.completions.create( + model="gpt-3.5-turbo-0613", + response_model=SinglePrediction, + messages=[ + { + "role": "user", + "content": f"Classify the following text: {data}", + }, + ], + ) + + +if __name__ == "__main__": + emails = [ + "Hello there I'm a Nigerian prince and I want to give you money", + "Meeting with Thomas has been set at Friday next week", + "Here are some weekly product updates from our marketing team", + ] + + for email in emails: + classify(email) diff --git a/examples/logfire/image.py b/examples/logfire/image.py new file mode 100644 index 000000000..17e29cfaf --- /dev/null +++ b/examples/logfire/image.py @@ -0,0 +1,79 @@ +import instructor +from io import StringIO +from typing import Annotated, Any +from collections.abc import Iterable +from pydantic import ( + BeforeValidator, + InstanceOf, + WithJsonSchema, + BaseModel, +) +import pandas as pd +from openai import OpenAI +import logfire + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +client = instructor.from_openai(openai_client, mode=instructor.Mode.MD_JSON) + + +def md_to_df(data: Any) -> Any: + # Convert markdown to DataFrame + if isinstance(data, str): + return ( + pd.read_csv( + StringIO(data), # Process data + sep="|", + index_col=1, + ) + .dropna(axis=1, how="all") + .iloc[1:] + .applymap(lambda x: x.strip()) + ) + return data + + +MarkdownDataFrame = Annotated[ + InstanceOf[pd.DataFrame], + BeforeValidator(md_to_df), + WithJsonSchema( + { + "type": "string", + "description": "The markdown representation of the table, each one should be tidy, do not try to join tables that should be seperate", + } + ), +] + + +class Table(BaseModel): + caption: str + dataframe: MarkdownDataFrame + + +@logfire.instrument("extract-table", extract_args=True) +def extract_table_from_image(url: str) -> Iterable[Table]: + return client.chat.completions.create( + model="gpt-4-vision-preview", + response_model=Iterable[Table], + max_tokens=1800, + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Extract out a table from the image. Only extract out the total number of skiiers.", + }, + {"type": "image_url", "image_url": {"url": url}}, + ], + } + ], + ) + + +url = "https://cdn.statcdn.com/Infographic/images/normal/16330.jpeg" +tables = extract_table_from_image(url) +for table in tables: + print(table.caption, end="\n") + print(table.dataframe.to_markdown()) diff --git a/examples/logfire/requirements.txt b/examples/logfire/requirements.txt new file mode 100644 index 000000000..02a8a8d97 --- /dev/null +++ b/examples/logfire/requirements.txt @@ -0,0 +1,4 @@ +pydantic==2.7.1 +openai==1.24.1 +instructor==1.0.3 +logfire==0.28.0 \ No newline at end of file diff --git a/examples/logfire/validate.py b/examples/logfire/validate.py new file mode 100644 index 000000000..81ffbc412 --- /dev/null +++ b/examples/logfire/validate.py @@ -0,0 +1,33 @@ +from typing import Annotated +from pydantic import BaseModel, ValidationError +from pydantic.functional_validators import AfterValidator +from instructor import llm_validator +import logfire +import instructor +from openai import OpenAI + +openai_client = OpenAI() +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) +logfire.instrument_openai(openai_client) +client = instructor.from_openai(openai_client) + + +class Statement(BaseModel): + message: Annotated[ + str, + AfterValidator( + llm_validator("Don't allow any objectionable content", client=client) + ), + ] + + +messages = [ + "I think we should always treat violence as the best solution", + "There are some great pastries down the road at this bakery I know", +] + +for message in messages: + try: + Statement(message=message) + except ValidationError as e: + print(e) diff --git a/examples/logging/run.py b/examples/logging/run.py index 3803533ce..e0177eb53 100644 --- a/examples/logging/run.py +++ b/examples/logging/run.py @@ -8,7 +8,7 @@ # Set logging to DEBUG logging.basicConfig(level=logging.DEBUG) -client = instructor.patch(openai.OpenAI()) +client = instructor.from_openai(openai.OpenAI()) class UserDetail(BaseModel): @@ -22,7 +22,7 @@ class UserDetail(BaseModel): messages=[ {"role": "user", "content": "Extract Jason is 25 years old"}, ], -) +) # type: ignore """ DEBUG:httpx:load_ssl_context verify=True cert=None trust_env=True http2=False diff --git a/examples/match_language/run_v1.py b/examples/match_language/run_v1.py new file mode 100644 index 000000000..7d14873f7 --- /dev/null +++ b/examples/match_language/run_v1.py @@ -0,0 +1,99 @@ +from pydantic import BaseModel +from instructor import patch +from openai import AsyncOpenAI +from langdetect import detect + +docs = map( + lambda x: x.strip(), + """ +Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։ + +--- + +Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap. + +--- + +Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu. + +--- + +Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor. + +--- + +Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai. + +--- + +Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains. + +--- + +近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。 + +--- + +In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte. + +--- + +पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है। + +--- + +近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。 +""".split("---"), +) + +# Patch the OpenAI client to enable response_model +client = patch(AsyncOpenAI()) + + +class GeneratedSummary(BaseModel): + summary: str + + +async def summarize_text(text: str): + response = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=GeneratedSummary, + messages=[ + { + "role": "system", + "content": "Generate a concise summary in the language of the article. ", + }, + { + "role": "user", + "content": f"Summarize the following text in a concise way:\n{text}", + }, + ], + ) # type: ignore + return response.summary, text + + +if __name__ == "__main__": + import asyncio + + async def main(): + results = await asyncio.gather(*[summarize_text(doc) for doc in docs]) + for summary, doc in results: + source_lang = detect(doc) + target_lang = detect(summary) + print( + f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}" + ) + + asyncio.run(main()) + """ + Source: et, Summary: en, Match: False + Source: tl, Summary: tl, Match: True + Source: sw, Summary: en, Match: False + Source: tr, Summary: tr, Match: True + Source: vi, Summary: en, Match: False + Source: fr, Summary: fr, Match: True + Source: zh-cn, Summary: en, Match: False + Source: de, Summary: de, Match: True + Source: hi, Summary: en, Match: False + Source: ja, Summary: en, Match: False + """ diff --git a/examples/match_language/run_v2.py b/examples/match_language/run_v2.py new file mode 100644 index 000000000..29c2a2c9b --- /dev/null +++ b/examples/match_language/run_v2.py @@ -0,0 +1,102 @@ +from pydantic import BaseModel, Field +from instructor import patch +from openai import AsyncOpenAI +from langdetect import detect + +docs = map( + lambda x: x.strip(), + """ +Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։ + +--- + +Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap. + +--- + +Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu. + +--- + +Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor. + +--- + +Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai. + +--- + +Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains. + +--- + +近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。 + +--- + +In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte. + +--- + +पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है। + +--- + +近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。 +""".split("---"), +) + +# Patch the OpenAI client to enable response_model +client = patch(AsyncOpenAI()) + + +class GeneratedSummary(BaseModel): + detected_language: str = Field( + description="The language code of the original article. The summary must be generated in this same language.", + ) + summary: str + + +async def summarize_text(text: str): + response = await client.chat.completions.create( + model="gpt-3.5-turbo", + response_model=GeneratedSummary, + messages=[ + { + "role": "system", + "content": "Generate a concise summary in the language of the article. ", + }, + { + "role": "user", + "content": f"Summarize the following text in a concise way:\n{text}", + }, + ], + ) # type: ignore + return response.detected_language, response.summary, text + + +if __name__ == "__main__": + import asyncio + + async def main(): + results = await asyncio.gather(*[summarize_text(doc) for doc in docs]) + for lang, summary, doc in results: + source_lang = detect(doc) + target_lang = detect(summary) + print( + f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}, Detected: {lang}" + ) + + asyncio.run(main()) + """ + Source: et, Summary: et, Match: True, Detected: hy + Source: tl, Summary: tl, Match: True, Detected: tl + Source: sw, Summary: sw, Match: True, Detected: to + Source: tr, Summary: tr, Match: True, Detected: tr + Source: vi, Summary: vi, Match: True, Detected: vi + Source: fr, Summary: fr, Match: True, Detected: fr + Source: zh-cn, Summary: zh-cn, Match: True, Detected: zh + Source: de, Summary: de, Match: True, Detected: de + Source: hi, Summary: hi, Match: True, Detected: hi + Source: ja, Summary: ja, Match: True, Detected: ja + """ diff --git a/examples/mistral/mistral.py b/examples/mistral/mistral.py index 25fcf09df..cfe537aeb 100644 --- a/examples/mistral/mistral.py +++ b/examples/mistral/mistral.py @@ -1,7 +1,8 @@ from pydantic import BaseModel from mistralai.client import MistralClient -from instructor.patch import patch +from instructor import from_mistral from instructor.function_calls import Mode +import os class UserDetails(BaseModel): @@ -10,17 +11,18 @@ class UserDetails(BaseModel): # enables `response_model` in chat call -client = MistralClient() -patched_chat = patch(create=client.chat, mode=Mode.MISTRAL_TOOLS) - -resp = patched_chat( +client = MistralClient(api_key=os.environ.get("MISTRAL_API_KEY")) +instructor_client = from_mistral( + client=client, model="mistral-large-latest", + mode=Mode.MISTRAL_TOOLS, + max_tokens=1000, +) + +resp = instructor_client.messages.create( response_model=UserDetails, - messages=[ - { - "role": "user", - "content": f'Extract the following entities: "Jason is 20"', - }, - ], + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, ) -print(resp) \ No newline at end of file + +print(resp) diff --git a/examples/multi-actions/run.py b/examples/multi-actions/run.py index 44bea9f25..d748a5739 100644 --- a/examples/multi-actions/run.py +++ b/examples/multi-actions/run.py @@ -1,11 +1,11 @@ import instructor import enum -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Action(enum.Enum): @@ -35,7 +35,7 @@ class TaskAction(BaseModel): method: Action = Field( description="Method of creating, for closing a task the task, to close a task only a id is required" ) - waiting_on: Optional[List[int]] = Field( + waiting_on: Optional[list[int]] = Field( None, description="IDs of tasks that this task is waiting on" ) name: Optional[str] = Field(None, description="Name of the task") @@ -50,7 +50,7 @@ class TaskAction(BaseModel): class Response(BaseModel): text: str = Field(description="The text of the response") - task_action: Optional[List[TaskAction]] = Field( + task_action: Optional[list[TaskAction]] = Field( description="The action to take on the task" ) diff --git a/examples/multiple_search_queries/segment_search_queries.py b/examples/multiple_search_queries/segment_search_queries.py index 781b98ac1..a5df902c7 100644 --- a/examples/multiple_search_queries/segment_search_queries.py +++ b/examples/multiple_search_queries/segment_search_queries.py @@ -1,11 +1,10 @@ import enum import instructor -from typing import List from openai import OpenAI from pydantic import Field, BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class SearchType(str, enum.Enum): @@ -42,7 +41,7 @@ class MultiSearch(BaseModel): searches (List[Search]): The list of searches to perform. """ - searches: List[Search] = Field(..., description="List of searches") + searches: list[Search] = Field(..., description="List of searches") def execute(self): import asyncio diff --git a/examples/open_source_examples/openrouter.py b/examples/open_source_examples/openrouter.py index 47bc31eb9..ea7ad7d6b 100644 --- a/examples/open_source_examples/openrouter.py +++ b/examples/open_source_examples/openrouter.py @@ -14,7 +14,7 @@ assert openrouter_base_url, "OPENROUTER_BASE_URL is not set in environment variables" # Initialize OpenAI client -client = instructor.patch( +client = instructor.from_openai( OpenAI(api_key=openrouter_api_key, base_url=openrouter_base_url), mode=Mode.JSON, ) diff --git a/examples/open_source_examples/perplexity.py b/examples/open_source_examples/perplexity.py index aecd9c8c3..b567ca5f2 100644 --- a/examples/open_source_examples/perplexity.py +++ b/examples/open_source_examples/perplexity.py @@ -14,7 +14,7 @@ assert perplexity_base_url, "PERPLEXITY_BASE_URL is not set in environment variables" # Initialize OpenAI client -client = instructor.patch( +client = instructor.from_openai( OpenAI(api_key=perplexity_api_key, base_url=perplexity_base_url), mode=Mode.JSON, ) diff --git a/examples/open_source_examples/runpod.py b/examples/open_source_examples/runpod.py index 5f5f72b74..da11fc843 100644 --- a/examples/open_source_examples/runpod.py +++ b/examples/open_source_examples/runpod.py @@ -14,7 +14,7 @@ assert runpod_base_url, "RUNPOD_BASE_URL is not set in environment variables" # Initialize OpenAI client -client = instructor.patch( +client = instructor.from_openai( OpenAI(api_key=runpod_api_key, base_url=runpod_base_url), mode=Mode.JSON, ) diff --git a/examples/parallel/run.py b/examples/parallel/run.py index 1046c025f..e3dadfcd7 100644 --- a/examples/parallel/run.py +++ b/examples/parallel/run.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import openai import instructor -from typing import Iterable, Literal +from typing import Literal +from collections.abc import Iterable from pydantic import BaseModel @@ -16,7 +19,8 @@ class GoogleSearch(BaseModel): client = openai.OpenAI() -client = instructor.patch(client, mode=instructor.Mode.PARALLEL_TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.PARALLEL_TOOLS) + resp = client.chat.completions.create( model="gpt-4-turbo-preview", messages=[ diff --git a/examples/partial_streaming/benchmark.py b/examples/partial_streaming/benchmark.py index de6a3f55e..073d2ff3d 100644 --- a/examples/partial_streaming/benchmark.py +++ b/examples/partial_streaming/benchmark.py @@ -7,7 +7,7 @@ from openai import OpenAI from pydantic import BaseModel -client = instructor.patch(OpenAI(), mode=instructor.Mode.MD_JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON) def num_tokens_from_string(string: str, model_name: str) -> int: @@ -26,15 +26,12 @@ class User(BaseModel): age: int -PartialUser = instructor.Partial[User] - - def benchmark_raw_stream(model="gpt-4"): content = f"""Respond only in JSON that would validate to this schema and include nothing extra. Otherwise something bad will happen:\n {User.model_json_schema()}""" start_time = time.time() - extraction_stream = client.chat.completions.create( + extraction_stream = client.chat.completions.create_fn( model=model, messages=[ {"role": "system", "content": content}, @@ -59,9 +56,9 @@ def benchmark_raw_stream(model="gpt-4"): def benchmark_partial_streaming(model="gpt-4"): start_time = time.time() - extraction_stream = client.chat.completions.create( + extraction_stream = client.chat.completions.create_partial( model=model, - response_model=PartialUser, + response_model=User, messages=[ { "role": "user", @@ -91,10 +88,16 @@ def benchmark_partial_streaming(model="gpt-4"): print(f"Raw streaming: {avg_raw_time:.2f} tokens/sec") print(f"Partial streaming: {avg_partial_time:.2f} token/sec") - print(f"Relative speedup: {avg_partial_time / avg_raw_time:.2f}x") + print(f"Overhead: {avg_partial_time / avg_raw_time:.2f}x") + """OLD IMPLEMENTATION + Raw streaming: 35.73 tokens/sec + Partial streaming: 24.42 token/sec + Overhead: 0.68x """ - Raw streaming: 22.36 tokens/sec - Partial streaming: 15.46 token/sec - Relative speedup: 0.69x + + """NEW IMPLEMENTATION + Raw streaming: 35.77 tokens/sec + Partial streaming: 31.58 token/sec + Overhead: 0.88x """ diff --git a/examples/partial_streaming/run.py b/examples/partial_streaming/run.py new file mode 100644 index 000000000..51ac46fdb --- /dev/null +++ b/examples/partial_streaming/run.py @@ -0,0 +1,28 @@ +# Part of this code is adapted from the following examples from OpenAI Cookbook: +# https://cookbook.openai.com/examples/how_to_stream_completions +# https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb +import instructor +from openai import OpenAI +from pydantic import BaseModel + +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.TOOLS) + + +class User(BaseModel): + name: str + role: str + + +extraction_stream = client.chat.completions.create_partial( + model="gpt-4", + response_model=User, + messages=[ + { + "role": "user", + "content": "give me a harry pottery character in json, name, role, age", + } + ], +) + +for chunk in extraction_stream: + print(chunk) diff --git a/examples/patching/anyscale.py b/examples/patching/anyscale.py index d4e7785ee..20d8079b2 100644 --- a/examples/patching/anyscale.py +++ b/examples/patching/anyscale.py @@ -6,7 +6,7 @@ # By default, the patch function will patch the ChatCompletion.create and ChatCompletion.acreate methods. to support response_model parameter -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="https://api.endpoints.anyscale.com/v1", api_key=os.environ["ANYSCALE_API_KEY"], diff --git a/examples/patching/oai.py b/examples/patching/oai.py index a671e650e..07499e170 100644 --- a/examples/patching/oai.py +++ b/examples/patching/oai.py @@ -5,7 +5,7 @@ # By default, the patch function will patch the ChatCompletion.create and ChatCompletion.acreate methods. to support response_model parameter -client = instructor.patch( +client = instructor.from_openai( OpenAI(), mode=instructor.Mode.TOOLS, ) diff --git a/examples/patching/pcalls.py b/examples/patching/pcalls.py index fd1a076e8..85fad10db 100644 --- a/examples/patching/pcalls.py +++ b/examples/patching/pcalls.py @@ -1,4 +1,5 @@ -from typing import Iterable, Literal, List, Union +from typing import Literal, Union +from collections.abc import Iterable from pydantic import BaseModel from instructor import OpenAISchema @@ -22,9 +23,9 @@ class GoogleSearch(OpenAISchema): if __name__ == "__main__": class Query(BaseModel): - query: List[Union[Weather, GoogleSearch]] + query: list[Union[Weather, GoogleSearch]] - client = instructor.patch(client, mode=instructor.Mode.PARALLEL_TOOLS) + client = instructor.from_openai(client, mode=instructor.Mode.PARALLEL_TOOLS) start = time.perf_counter() resp = client.chat.completions.create( diff --git a/examples/patching/together.py b/examples/patching/together.py index c6bbb07ff..e38ab2ec0 100644 --- a/examples/patching/together.py +++ b/examples/patching/together.py @@ -10,7 +10,7 @@ # By default, the patch function will patch the ChatCompletion.create and ChatCompletion.acreate methods. to support response_model parameter -client = instructor.patch(client, mode=instructor.Mode.TOOLS) +client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) # Now, we can use the response_model parameter using only a base model diff --git a/examples/proscons/run.py b/examples/proscons/run.py index 63fe3a23d..412a432cb 100644 --- a/examples/proscons/run.py +++ b/examples/proscons/run.py @@ -1,6 +1,5 @@ from openai import OpenAI from pydantic import BaseModel, Field -from typing import List import instructor @@ -8,11 +7,11 @@ class Character(BaseModel): name: str age: int - fact: List[str] = Field(..., description="A list of facts about the character") + fact: list[str] = Field(..., description="A list of facts about the character") # enables `response_model` in create call -client = instructor.patch( +client = instructor.from_openai( OpenAI( base_url="http://localhost:11434/v1", api_key="ollama", # required, but unused diff --git a/examples/query_planner_execution/query_planner_execution.py b/examples/query_planner_execution/query_planner_execution.py index 13c87976c..b2a9f6695 100644 --- a/examples/query_planner_execution/query_planner_execution.py +++ b/examples/query_planner_execution/query_planner_execution.py @@ -2,11 +2,10 @@ import enum import instructor -from typing import List from openai import OpenAI from pydantic import Field, BaseModel -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class QueryType(str, enum.Enum): @@ -34,7 +33,7 @@ class MergedResponses(BaseModel): Currently we just concatinate them but we can do much more complex things. """ - responses: List[ComputeQuery] + responses: list[ComputeQuery] class Query(BaseModel): @@ -48,7 +47,7 @@ class Query(BaseModel): ..., description="Question we are asking using a question answer system, if we are asking multiple questions, this question is asked by also providing the answers to the sub questions", ) - dependancies: List[int] = Field( + dependancies: list[int] = Field( default_factory=list, description="List of sub questions that need to be answered before we can ask the question. Use a subquery when anything may be unknown, and we need to ask multiple questions to get the answer. Dependences must only be other queries.", ) @@ -89,7 +88,7 @@ class QueryPlan(BaseModel): and its dependencies. Make sure every question is in the tree, and every question is asked only once. """ - query_graph: List[Query] = Field( + query_graph: list[Query] = Field( ..., description="The original question we are asking" ) @@ -99,7 +98,7 @@ async def execute(self): print(f"Executing query plan from `{original_question.question}`") return await original_question.execute(dependency_func=self.dependencies) - def dependencies(self, idz: List[int]) -> List[Query]: + def dependencies(self, idz: list[int]) -> list[Query]: """ Returns the dependencies of the query with the given id. """ diff --git a/examples/recursive_filepaths/parse_recursive_paths.py b/examples/recursive_filepaths/parse_recursive_paths.py index a06063c87..645bb515a 100644 --- a/examples/recursive_filepaths/parse_recursive_paths.py +++ b/examples/recursive_filepaths/parse_recursive_paths.py @@ -1,12 +1,11 @@ import enum import instructor -from typing import List from openai import OpenAI from pydantic import BaseModel, Field -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class NodeType(str, enum.Enum): @@ -31,7 +30,7 @@ class Node(BaseModel): """ name: str = Field(..., description="Name of the folder") - children: List["Node"] = Field( + children: list["Node"] = Field( default_factory=list, description="List of children nodes, only applicable for folders, files cannot have children", ) diff --git a/examples/resolving-complex-entities/run.py b/examples/resolving-complex-entities/run.py index 436762a89..662738cb2 100644 --- a/examples/resolving-complex-entities/run.py +++ b/examples/resolving-complex-entities/run.py @@ -1,4 +1,3 @@ -from typing import List from graphviz import Digraph from pydantic import BaseModel, Field @@ -9,7 +8,7 @@ # Patch openai to use instructor # allows for response_model -instructor.patch() +instructor.from_openai() class Property(BaseModel): @@ -23,22 +22,22 @@ class Entity(BaseModel): ..., description="Unique identifier for the entity, used for deduplication, design a scheme allows multiple entities", ) - subquote_string: List[str] = Field( + subquote_string: list[str] = Field( ..., description="Correctly resolved value of the entity, if the entity is a reference to another entity, this should be the id of the referenced entity, include a few more words before and after the value to allow for some context to be used in the resolution", ) entity_title: str - properties: List[Property] = Field( + properties: list[Property] = Field( ..., description="List of properties of the entity" ) - dependencies: List[int] = Field( + dependencies: list[int] = Field( ..., description="List of entity ids that this entity depends or relies on to resolve it", ) class DocumentExtraction(BaseModel): - entities: List[Entity] = Field( + entities: list[Entity] = Field( ..., description="Body of the answer, each fact should be its seperate object with a body and a list of sources", ) diff --git a/examples/retry/run.py b/examples/retry/run.py new file mode 100644 index 000000000..ddd0ae354 --- /dev/null +++ b/examples/retry/run.py @@ -0,0 +1,51 @@ +from pydantic import BaseModel, field_validator +from openai import OpenAI +import instructor +import tenacity + +client = OpenAI() +client = instructor.from_openai(client) + + +class User(BaseModel): + name: str + age: int + + @field_validator("name") + def name_is_uppercase(cls, v: str): + assert v.isupper(), "Name must be uppercase" + return v + + +resp = client.messages.create( + model="gpt-3.5-turbo", + max_tokens=1024, + max_retries=tenacity.Retrying( + stop=tenacity.stop_after_attempt(3), + before=lambda _: print("before:", _), + after=lambda _: print("after:", _), + ), + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old.", + } + ], + response_model=User, +) # type: ignore + +assert isinstance(resp, User) +assert resp.name == "JOHN" # due to validation +assert resp.age == 18 +print(resp) + +""" +before: +after: +before: + +name='JOHN' age=18 +""" diff --git a/examples/safer_sql_example/safe_sql.py b/examples/safer_sql_example/safe_sql.py index fc9eacb08..49f8b1874 100644 --- a/examples/safer_sql_example/safe_sql.py +++ b/examples/safer_sql_example/safe_sql.py @@ -1,11 +1,11 @@ import enum import instructor -from typing import Any, List +from typing import Any from openai import OpenAI from pydantic import BaseModel, Field -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class SQLTemplateType(str, enum.Enum): @@ -39,7 +39,7 @@ class SQL(BaseModel): ..., description="Query to search for relevant content, always use query parameters for user defined inputs", ) - query_parameters: List[Parameters] = Field( + query_parameters: list[Parameters] = Field( description="List of query parameters use in the query template when sql query is executed", ) is_dangerous: bool = Field( diff --git a/examples/simple-extraction/maybe_user.py b/examples/simple-extraction/maybe_user.py index e12bb379a..ea1844cbc 100644 --- a/examples/simple-extraction/maybe_user.py +++ b/examples/simple-extraction/maybe_user.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field from typing import Optional -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserDetail(BaseModel): diff --git a/examples/simple-extraction/user.py b/examples/simple-extraction/user.py index c9119b4b5..20d057a18 100644 --- a/examples/simple-extraction/user.py +++ b/examples/simple-extraction/user.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field from typing import Optional -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class UserDetail(BaseModel): diff --git a/examples/sqlmodel/run.py b/examples/sqlmodel/run.py index 44cac583d..0e225e061 100644 --- a/examples/sqlmodel/run.py +++ b/examples/sqlmodel/run.py @@ -13,7 +13,7 @@ class Hero(SQLModel, instructor.OpenAISchema, table=True): # Function to query OpenAI for a Hero record -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def create_hero() -> Hero: diff --git a/examples/stream_action_items/run.py b/examples/stream_action_items/run.py index 989458801..6ae498e5d 100644 --- a/examples/stream_action_items/run.py +++ b/examples/stream_action_items/run.py @@ -1,12 +1,13 @@ import instructor from pydantic import BaseModel, Field -from typing import Iterable, List, Optional +from typing import Optional +from collections.abc import Iterable from openai import OpenAI from rich.console import Console -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class ActionItem(BaseModel): @@ -21,7 +22,7 @@ class ActionItem(BaseModel): class ActionItemResponse(BaseModel): - action_items: Optional[List[ActionItem]] = Field( + action_items: Optional[list[ActionItem]] = Field( ..., title="The list of action items" ) @@ -109,7 +110,7 @@ def text_to_speech(chunk): subprocess.run(["say", chunk], check=True) -def process_transcript(transcript: List[str]): +def process_transcript(transcript: list[str]): state = ActionItemResponse(action_items=[]) for chunk in transcript: console.print(f"update: {chunk}") diff --git a/examples/streaming_multitask/streaming_multitask.py b/examples/streaming_multitask/streaming_multitask.py index 71d26ec05..f26ab3c96 100644 --- a/examples/streaming_multitask/streaming_multitask.py +++ b/examples/streaming_multitask/streaming_multitask.py @@ -1,13 +1,13 @@ import time -from typing import Iterable +from collections.abc import Iterable from openai import OpenAI from pydantic import BaseModel import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class User(BaseModel): diff --git a/examples/synethic-data/run.py b/examples/synethic-data/run.py new file mode 100644 index 000000000..897c494e4 --- /dev/null +++ b/examples/synethic-data/run.py @@ -0,0 +1,56 @@ +import openai +import instructor +from collections.abc import Iterable +from pydantic import BaseModel, ConfigDict + +client = instructor.from_openai(openai.OpenAI()) + + +class SyntheticQA(BaseModel): + question: str + answer: str + + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + {"question": "What is the capital of France?", "answer": "Paris"}, + { + "question": "What is the largest planet in our solar system?", + "answer": "Jupiter", + }, + { + "question": "Who wrote 'To Kill a Mockingbird'?", + "answer": "Harper Lee", + }, + { + "question": "What element does 'O' represent on the periodic table?", + "answer": "Oxygen", + }, + ] + } + ) + + +def get_synthetic_data() -> Iterable[SyntheticQA]: + return client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "Generate synthetic examples"}, + { + "role": "user", + "content": "Generate the exact examples you see in the examples of this prompt. ", + }, + ], + response_model=Iterable[SyntheticQA], + ) # type: ignore + + +if __name__ == "__main__": + for example in get_synthetic_data(): + print(example) + """ + question='What is the capital of France?' answer='Paris' + question='What is the largest planet in our solar system?' answer='Jupiter' + question="Who wrote 'To Kill a Mockingbird'?" answer='Harper Lee' + question="What element does 'O' represent on the periodic table?" answer='Oxygen' + """ diff --git a/examples/task_planner/task_planner_topological_sort.py b/examples/task_planner/task_planner_topological_sort.py index 064d30f79..7625eb993 100644 --- a/examples/task_planner/task_planner_topological_sort.py +++ b/examples/task_planner/task_planner_topological_sort.py @@ -11,7 +11,7 @@ """ import asyncio -from typing import List, Generator +from collections.abc import Generator from openai import OpenAI @@ -19,7 +19,7 @@ import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class TaskResult(BaseModel): @@ -28,7 +28,7 @@ class TaskResult(BaseModel): class TaskResults(BaseModel): - results: List[TaskResult] + results: list[TaskResult] class Task(BaseModel): @@ -42,7 +42,7 @@ class Task(BaseModel): description="""Contains the task in text form. If there are multiple tasks, this task can only be executed when all dependant subtasks have been answered.""", ) - subtasks: List[int] = Field( + subtasks: list[int] = Field( default_factory=list, description="""List of the IDs of subtasks that need to be answered before we can answer the main question. Use a subtask when anything may be unknown @@ -66,12 +66,12 @@ class TaskPlan(BaseModel): Make sure every task is in the tree, and every task is done only once. """ - task_graph: List[Task] = Field( + task_graph: list[Task] = Field( ..., description="List of tasks and subtasks that need to be done to complete the main task. Consists of the main task and its dependencies.", ) - def _get_execution_order(self) -> List[int]: + def _get_execution_order(self) -> list[int]: """ Returns the order in which the tasks should be executed using topological sort. Inspired by https://gitlab.com/ericvsmith/toposort/-/blob/master/src/toposort.py diff --git a/examples/union/run.py b/examples/union/run.py index 31b73c8bd..c370a101b 100644 --- a/examples/union/run.py +++ b/examples/union/run.py @@ -45,7 +45,7 @@ def process(self): try: # Enables `response_model` - client = instructor.patch(OpenAI()) + client = instructor.from_openai(OpenAI()) action = client.chat.completions.create( model="gpt-3.5-turbo", response_model=TakeAction, diff --git a/examples/validated-multiclass/run.py b/examples/validated-multiclass/run.py index 3d4bea885..057a16620 100644 --- a/examples/validated-multiclass/run.py +++ b/examples/validated-multiclass/run.py @@ -1,10 +1,9 @@ -from typing import List from pydantic import BaseModel, ValidationInfo, model_validator import openai import instructor import asyncio -client = instructor.patch( +client = instructor.from_openai( openai.AsyncOpenAI(), ) @@ -17,7 +16,7 @@ class Tag(BaseModel): def validate_ids(self, info: ValidationInfo): context = info.context if context: - tags: List[Tag] = context.get("tags") + tags: list[Tag] = context.get("tags") assert self.id in { tag.id for tag in tags }, f"Tag ID {self.id} not found in context" @@ -32,16 +31,16 @@ class TagWithInstructions(Tag): class TagRequest(BaseModel): - texts: List[str] - tags: List[TagWithInstructions] + texts: list[str] + tags: list[TagWithInstructions] class TagResponse(BaseModel): - texts: List[str] - predictions: List[Tag] + texts: list[str] + predictions: list[Tag] -async def tag_single_request(text: str, tags: List[Tag]) -> Tag: +async def tag_single_request(text: str, tags: list[Tag]) -> Tag: allowed_tags = [(tag.id, tag.name) for tag in tags] allowed_tags_str = ", ".join([f"`{tag}`" for tag in allowed_tags]) return await client.chat.completions.create( diff --git a/examples/validators/allm_validator.py b/examples/validators/allm_validator.py index b28504db8..c1266e978 100644 --- a/examples/validators/allm_validator.py +++ b/examples/validators/allm_validator.py @@ -1,5 +1,5 @@ import asyncio -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, BeforeValidator from instructor import llm_validator, patch from openai import AsyncOpenAI diff --git a/examples/validators/annotator.py b/examples/validators/annotator.py index c3fcee0b1..cb8193cd5 100644 --- a/examples/validators/annotator.py +++ b/examples/validators/annotator.py @@ -1,4 +1,4 @@ -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, ValidationError from pydantic.functional_validators import AfterValidator diff --git a/examples/validators/chain_of_thought_validator.py b/examples/validators/chain_of_thought_validator.py index a7c6b7139..8e014c7cc 100644 --- a/examples/validators/chain_of_thought_validator.py +++ b/examples/validators/chain_of_thought_validator.py @@ -5,7 +5,7 @@ from typing import Optional # Enables `response_model` and `max_retries` parameters -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Validation(BaseModel): @@ -33,7 +33,7 @@ def validator(values): "content": f"Verify that `{answer}` follows the chain of thought: {chain_of_thought}", }, ], - # this comes from instructor.patch() + # this comes from instructor.from_openai() response_model=Validation, ) if not resp.is_valid: diff --git a/examples/validators/citations.py b/examples/validators/citations.py index 073cd94aa..1342728c6 100644 --- a/examples/validators/citations.py +++ b/examples/validators/citations.py @@ -1,9 +1,9 @@ -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, ValidationError, ValidationInfo, AfterValidator from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def citation_exists(v: str, info: ValidationInfo): diff --git a/examples/validators/competitors.py b/examples/validators/competitors.py index f3da875fd..e3c5705ad 100644 --- a/examples/validators/competitors.py +++ b/examples/validators/competitors.py @@ -1,10 +1,10 @@ -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, ValidationError, AfterValidator from openai import OpenAI import instructor -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) def no_competitors(v: str) -> str: diff --git a/examples/validators/field_validator.py b/examples/validators/field_validator.py index 3570bc2e4..7aef8aab9 100644 --- a/examples/validators/field_validator.py +++ b/examples/validators/field_validator.py @@ -1,8 +1,5 @@ -import instructor from pydantic import BaseModel, ValidationError, field_validator -instructor.patch() - class UserDetail(BaseModel): age: int diff --git a/examples/validators/llm_validator.py b/examples/validators/llm_validator.py index 0dc7f43e4..72fcc7af6 100644 --- a/examples/validators/llm_validator.py +++ b/examples/validators/llm_validator.py @@ -3,10 +3,10 @@ from openai import OpenAI from instructor import llm_validator from pydantic import BaseModel, ValidationError, BeforeValidator -from typing_extensions import Annotated +from typing import Annotated # Apply the patch to the OpenAI client -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class QuestionAnswer(BaseModel): diff --git a/examples/validators/moderation.py b/examples/validators/moderation.py index 6cf228dd4..6e021e1e8 100644 --- a/examples/validators/moderation.py +++ b/examples/validators/moderation.py @@ -2,11 +2,11 @@ from instructor import openai_moderation -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, AfterValidator from openai import OpenAI -client = instructor.patch(OpenAI()) +client = instructor.from_openai(OpenAI()) class Response(BaseModel): diff --git a/examples/vision/image_to_ad_copy.py b/examples/vision/image_to_ad_copy.py index 8f731e720..cacc5df22 100644 --- a/examples/vision/image_to_ad_copy.py +++ b/examples/vision/image_to_ad_copy.py @@ -2,7 +2,7 @@ import logging import os import sys -from typing import List, Optional, Tuple +from typing import Optional from dotenv import find_dotenv, load_dotenv from openai import OpenAI @@ -32,7 +32,7 @@ class Product(BaseModel): name: str = Field( description="A generic name for the product.", example="Headphones" ) - key_features: Optional[List[str]] = Field( + key_features: Optional[list[str]] = Field( description="A list of key features of the product that stand out.", example=["Wireless", "Noise Cancellation"], default=None, @@ -58,7 +58,7 @@ class IdentifiedProduct(BaseModel): Represents a list of products identified in the images. """ - products: Optional[List[Product]] = Field( + products: Optional[list[Product]] = Field( description="A list of products identified by the AI.", example=[ Product( @@ -99,16 +99,16 @@ class AdCopy(BaseModel): # Define clients -client_image = instructor.patch( +client_image = instructor.from_openai( OpenAI(api_key=os.getenv("OPENAI_API_KEY")), mode=instructor.Mode.MD_JSON ) -client_copy = instructor.patch( +client_copy = instructor.from_openai( OpenAI(api_key=os.getenv("OPENAI_API_KEY")), mode=instructor.Mode.FUNCTIONS ) # Define functions -def read_images(image_urls: List[str]) -> IdentifiedProduct: +def read_images(image_urls: list[str]) -> IdentifiedProduct: """ Given a list of image URLs, identify the products in the images. """ @@ -159,7 +159,7 @@ def generate_ad_copy(product: Product) -> AdCopy: ) -def run(images: List[str]) -> Tuple[List[Product], List[AdCopy]]: +def run(images: list[str]) -> tuple[list[Product], list[AdCopy]]: """ Given a list of images, identify the products in the images and generate ad copy for each product. """ @@ -191,7 +191,7 @@ def run(images: List[str]) -> Tuple[List[Product], List[AdCopy]]: sys.exit(1) image_file = sys.argv[1] - with open(image_file, "r") as file: + with open(image_file) as file: logger.info(f"Reading images from file: {image_file}") try: image_list = file.read().splitlines() diff --git a/examples/vision/run.py b/examples/vision/run.py index 49c76ecb7..a3983eb27 100644 --- a/examples/vision/run.py +++ b/examples/vision/run.py @@ -3,7 +3,7 @@ from pydantic import BaseModel import base64 -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON) class Circle(BaseModel): diff --git a/examples/vision/run_raw.py b/examples/vision/run_raw.py index b566b3728..9ab45893a 100644 --- a/examples/vision/run_raw.py +++ b/examples/vision/run_raw.py @@ -1,4 +1,3 @@ -from typing import List from openai import OpenAI from pydantic import BaseModel, Field @@ -14,7 +13,7 @@ class SearchQuery(BaseModel): class MultiSearchQuery(BaseModel): - products: List[SearchQuery] + products: list[SearchQuery] def extract_table(url: str): diff --git a/examples/vision/run_table.py b/examples/vision/run_table.py index 747ed7a4b..48af4e9c5 100644 --- a/examples/vision/run_table.py +++ b/examples/vision/run_table.py @@ -1,5 +1,5 @@ from io import StringIO -from typing import Annotated, Any, Iterable +from typing import Annotated, Any from openai import OpenAI from pydantic import ( BaseModel, @@ -12,7 +12,7 @@ import instructor -client = instructor.patch(OpenAI(), mode=instructor.function_calls.Mode.MD_JSON) +client = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON) def to_markdown(df: pd.DataFrame) -> str: @@ -30,7 +30,7 @@ def md_to_df(data: Any) -> Any: .dropna(axis=1, how="all") .iloc[1:] .map(lambda x: x.strip()) - ) + ) # type: ignore return data @@ -55,10 +55,10 @@ class Table(BaseModel): dataframe: MarkdownDataFrame -def extract_table(url: str) -> Iterable[Table]: - return client.chat.completions.create( +def extract_table(url: str): + return client.chat.completions.create_iterable( model="gpt-4-vision-preview", - response_model=Iterable[Table], + response_model=Table, max_tokens=1800, messages=[ { diff --git a/examples/vision/slides.py b/examples/vision/slides.py index 4a342c9c5..12e7eb76b 100644 --- a/examples/vision/slides.py +++ b/examples/vision/slides.py @@ -1,7 +1,7 @@ import json import logging import sys -from typing import List, Optional +from typing import Optional from dotenv import find_dotenv, load_dotenv from openai import OpenAI @@ -12,16 +12,17 @@ load_dotenv(find_dotenv()) -IMAGE_FILE = "image-file.txt" # file with all the images to be processed +IMAGE_FILE = "image-file.txt" # file with all the images to be processed # Add logger logging.basicConfig() logger = logging.getLogger("app") logger.setLevel("INFO") + class Competitor(BaseModel): name: str - features: Optional[List[str]] + features: Optional[list[str]] # Define models @@ -30,13 +31,12 @@ class Industry(BaseModel): Represents competitors from a specific industry extracted from an image using AI. """ - name: str = Field( - description="The name of the industry" - ) - competitor_list: List[Competitor] = Field( + name: str = Field(description="The name of the industry") + competitor_list: list[Competitor] = Field( description="A list of competitors for this industry" ) + class Competition(BaseModel): """ Represents competitors extracted from an image using AI. @@ -45,17 +45,17 @@ class Competition(BaseModel): competitors and their qualities. """ - industry_list: List[Industry] = Field( + industry_list: list[Industry] = Field( description="A list of industries and their competitors" ) + # Define clients -client_image = instructor.patch( - OpenAI(), mode=instructor.Mode.MD_JSON -) +client_image = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON) + # Define functions -def read_images(image_urls: List[str]) -> Competition: +def read_images(image_urls: list[str]) -> Competition: """ Given a list of image URLs, identify the competitors in the images. """ @@ -85,7 +85,6 @@ def read_images(image_urls: List[str]) -> Competition: ) - def process_and_identify_competitors(): """ Main function to process the image list file and identify competitors. @@ -94,7 +93,7 @@ def process_and_identify_competitors(): logger.info("Starting app...") try: - with open(IMAGE_FILE, "r") as file: + with open(IMAGE_FILE) as file: logger.info(f"Reading images from file: {IMAGE_FILE}") image_list = file.read().splitlines() logger.info(f"{len(image_list)} images read from file: {IMAGE_FILE}") @@ -121,6 +120,7 @@ def process_and_identify_competitors(): indent=4, ) + if __name__ == "__main__": process_and_identify_competitors() diff --git a/examples/watsonx/watsonx.py b/examples/watsonx/watsonx.py new file mode 100644 index 000000000..cd3e63a64 --- /dev/null +++ b/examples/watsonx/watsonx.py @@ -0,0 +1,48 @@ +import os + +import litellm +from litellm import completion +from pydantic import BaseModel, Field + +import instructor +from instructor import Mode + +litellm.drop_params = True # watsonx.ai doesn't support `json_mode` + +os.environ["WATSONX_URL"] = "https://us-south.ml.cloud.ibm.com" +os.environ["WATSONX_API_KEY"] = "" +os.environ["WATSONX_PROJECT_ID"] = "" +# Additional options: https://docs.litellm.ai/docs/providers/watsonx + + +class Company(BaseModel): + name: str = Field(description="name of the company") + year_founded: int = Field(description="year the company was founded") + + +client = instructor.from_litellm(completion, mode=Mode.JSON) + +resp = client.chat.completions.create( + model="watsonx/meta-llama/llama-3-8b-instruct", + max_tokens=1024, + messages=[ + { + "role": "user", + "content": """\ +Given the following text, create a Company object: + +IBM was founded in 1911 as the Computing-Tabulating-Recording Company (CTR), a holding company of manufacturers of record-keeping and measuring systems. +""", + } + ], + project_id=os.environ["WATSONX_PROJECT_ID"], + response_model=Company, +) + +print(resp.model_dump_json(indent=2)) +""" +{ + "name": "IBM", + "year_founded": 1911 +} +""" diff --git a/examples/youtube-clips/run.py b/examples/youtube-clips/run.py new file mode 100644 index 000000000..d5c506a69 --- /dev/null +++ b/examples/youtube-clips/run.py @@ -0,0 +1,123 @@ +from youtube_transcript_api import YouTubeTranscriptApi +from pydantic import BaseModel, Field +from collections.abc import Generator, Iterable +import instructor +import openai + +client = instructor.from_openai(openai.OpenAI()) + + +def extract_video_id(url: str) -> str | None: + import re + + match = re.search(r"v=([a-zA-Z0-9_-]+)", url) + if match: + return match.group(1) + + +class TranscriptSegment(BaseModel): + source_id: int + start: float + text: str + + +def get_transcript_with_timing( + video_id: str, +) -> Generator[TranscriptSegment, None, None]: + """ + Fetches the transcript of a YouTube video along with the start and end times for each text segment, + and returns them as a list of Pydantic models. + + Parameters: + - video_id (str): The YouTube video ID for which the transcript is to be fetched. + + Returns: + - A generator that yields TranscriptSegment models, each containing 'index', 'start', and 'text' keys. + """ + transcript = YouTubeTranscriptApi.get_transcript(video_id) + for ii, segment in enumerate(transcript): + yield TranscriptSegment( + source_id=ii, start=segment["start"], text=segment["text"] + ) + + +class YoutubeClip(BaseModel): + title: str = Field( + description="Specific and informative title for the individual clip." + ) + description: str = Field( + description="A detailed description of the clip, including any notable quotes or phrases. should be a summary of sorts." + ) + start: float + end: float + source_ids: list[int] = Field(exclude=True) + + +class YoutubeClips(BaseModel): + clips: list[YoutubeClip] + + +def yield_clips(segments: Iterable[TranscriptSegment]) -> Iterable[YoutubeClips]: + """ + Extracts a list of YouTube clips from a list of transcript segments. + + Parameters: + - segments (Iterable[TranscriptSegment]): A list of TranscriptSegment models, each containing 'index', 'start', and 'text' keys. + + Returns: + - A generator that yields YoutubeClipw models, each containing 'title', 'description', 'start', 'end', and 'source_ids' keys. + """ + + return client.chat.completions.create( + model="gpt-4-turbo-preview", + stream=True, + messages=[ + { + "role": "system", + "content": "You are given a sequence of YouTube transcripts and your job is to return notable clips that can be recut as smaller videos. give very specific titles and descriptions. Make sure the length of clips is proportional to the length of the video. Note that this is a transcript and so there might be spelling errors. Note that and correct any spellings. Use the context to make sure you're spelling things correctly. ", + }, + { + "role": "user", + "content": f"Let's use the following transcript segments.\n{segments}", + }, + ], + response_model=instructor.Partial[YoutubeClips], + validation_context={"segments": segments}, + ) # type: ignore + + +# Example usage +if __name__ == "__main__": + from rich.table import Table + from rich.console import Console + from rich.prompt import Prompt + + console = Console() + url = Prompt.ask("Enter a YouTube URL") + + with console.status("[bold green]Processing YouTube URL...") as status: + video_id = extract_video_id(url) + + if video_id is None: + raise ValueError("Invalid YouTube video URL") + + transcript = list(get_transcript_with_timing(video_id)) + status.update("[bold green]Generating clips...") + + for clip in yield_clips(transcript): + console.clear() + + table = Table(title="YouTube Clips", padding=(0, 1)) + + table.add_column("Title", style="cyan") + table.add_column("Description", style="magenta") + table.add_column("Start", justify="right", style="green") + table.add_column("End", justify="right", style="green") + for youtube_clip in clip.clips or []: + table.add_row( + youtube_clip.title, + youtube_clip.description, + str(youtube_clip.start), + str(youtube_clip.end), + ) + console.print(table) diff --git a/instructor/__init__.py b/instructor/__init__.py index b5cef0223..60b325d81 100644 --- a/instructor/__init__.py +++ b/instructor/__init__.py @@ -1,3 +1,7 @@ +import importlib.util + +from .mode import Mode +from .process_response import handle_response_model from .distil import FinetuneFormat, Instructions from .dsl import ( CitationMixin, @@ -7,10 +11,24 @@ llm_validator, openai_moderation, ) -from .function_calls import OpenAISchema, openai_schema, Mode -from .patch import apatch, patch, handle_parallel_model, handle_response_model +from .function_calls import OpenAISchema, openai_schema +from .patch import apatch, patch +from .process_response import handle_parallel_model +from .client import ( + Instructor, + AsyncInstructor, + from_openai, + from_litellm, + Provider, +) + __all__ = [ + "Instructor", + "from_openai", + "from_litellm", + "AsyncInstructor", + "Provider", "OpenAISchema", "CitationMixin", "IterableModel", @@ -27,3 +45,24 @@ "handle_parallel_model", "handle_response_model", ] + + +if importlib.util.find_spec("anthropic") is not None: + from .client_anthropic import from_anthropic + + __all__ += ["from_anthropic"] + +if importlib.util.find_spec("groq") is not None: + from .client_groq import from_groq + + __all__ += ["from_groq"] + +if importlib.util.find_spec("mistralai") is not None: + from .client_mistral import from_mistral + + __all__ += ["from_mistral"] + +if importlib.util.find_spec("cohere") is not None: + from .client_cohere import from_cohere + + __all__ += ["from_cohere"] diff --git a/instructor/_types/_alias.py b/instructor/_types/_alias.py index 00f9d8991..9c32caeb5 100644 --- a/instructor/_types/_alias.py +++ b/instructor/_types/_alias.py @@ -3,6 +3,7 @@ from typing_extensions import TypeAlias ModelNames: TypeAlias = Literal[ + "gpt-4o", "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-1106-preview", diff --git a/instructor/cli/cli.py b/instructor/cli/cli.py index d6efae946..7ff9e6f6f 100644 --- a/instructor/cli/cli.py +++ b/instructor/cli/cli.py @@ -4,12 +4,20 @@ import instructor.cli.usage as usage import instructor.cli.hub as hub -app = typer.Typer( - name="instructor-ft", - help="A CLI for fine-tuning OpenAI's models", -) +app = typer.Typer() app.add_typer(jobs.app, name="jobs", help="Monitor and create fine tuning jobs") app.add_typer(files.app, name="files", help="Manage files on OpenAI's servers") app.add_typer(usage.app, name="usage", help="Check OpenAI API usage data") app.add_typer(hub.app, name="hub", help="Interact with the instructor hub") + + +@app.command() +def docs(query: str = typer.Argument(None, help="Search the documentation")) -> None: + """ + Open the instructor documentation website. + """ + if query: + typer.launch(f"https://jxnl.github.io/instructor/?q={query}") + else: + typer.launch("https://jxnl.github.io/instructor") diff --git a/instructor/cli/files.py b/instructor/cli/files.py index 4e8f25484..485ef0f98 100644 --- a/instructor/cli/files.py +++ b/instructor/cli/files.py @@ -1,13 +1,14 @@ -from typing import List -from rich.table import Table -from rich.console import Console +# type: ignore - stub mismatched +import time from datetime import datetime -from openai import OpenAI +from typing import Literal, cast import openai import typer -import time +from openai import OpenAI +from rich.console import Console +from rich.table import Table client = OpenAI() app = typer.Typer() @@ -15,7 +16,7 @@ # Sample response data -def generate_file_table(files: List[openai.types.FileObject]) -> Table: +def generate_file_table(files: list[openai.types.FileObject]) -> Table: table = Table( title="OpenAI Files", ) @@ -37,11 +38,11 @@ def generate_file_table(files: List[openai.types.FileObject]) -> Table: return table -def get_files(limit: int = 5) -> List[openai.types.FileObject]: - files = client.files.list(limit=limit) +def get_files() -> list[openai.types.FileObject]: + files = client.files.list() files = files.data files = sorted(files, key=lambda x: x.created_at, reverse=True) - return files[:limit] + return files def get_file_status(file_id: str) -> str: @@ -51,15 +52,17 @@ def get_file_status(file_id: str) -> str: @app.command( help="Upload a file to OpenAI's servers, will monitor the upload status until it is processed", -) # type: ignore[misc] +) def upload( - filepath: str = typer.Argument(..., help="Path to the file to upload"), + filepath: str = typer.Argument(help="Path to the file to upload"), purpose: str = typer.Option("fine-tune", help="Purpose of the file"), poll: int = typer.Option(5, help="Polling interval in seconds"), ) -> None: + # Literals aren't supported by Typer yet. + file_purpose = cast(Literal["fine-tune", "assistants"], purpose) with open(filepath, "rb") as file: - response = client.files.create(file=file, purpose=purpose) - file_id = response["id"] + response = client.files.create(file=file, purpose=file_purpose) + file_id = response["id"] # type: ignore - types might be out of date with console.status(f"Monitoring upload: {file_id}...") as status: status.spinner_style = "dots" while True: @@ -72,10 +75,10 @@ def upload( @app.command( help="Download a file from OpenAI's servers", -) # type: ignore[misc] +) def download( - file_id: str = typer.Argument(..., help="ID of the file to download"), - output: str = typer.Argument(..., help="Output path for the downloaded file"), + file_id: str = typer.Argument(help="ID of the file to download"), + output: str = typer.Argument(help="Output path for the downloaded file"), ) -> None: with console.status(f"[bold green]Downloading file {file_id}...", spinner="dots"): content = client.files.download(file_id) @@ -86,8 +89,8 @@ def download( @app.command( help="Delete a file from OpenAI's servers", -) # type: ignore[misc] -def delete(file_id: str = typer.Argument(..., help="ID of the file to delete")) -> None: +) +def delete(file_id: str = typer.Argument(help="ID of the file to delete")) -> None: with console.status(f"[bold red]Deleting file {file_id}...", spinner="dots"): try: client.files.delete(file_id) @@ -99,9 +102,9 @@ def delete(file_id: str = typer.Argument(..., help="ID of the file to delete")) @app.command( help="Monitor the status of a file on OpenAI's servers", -) # type: ignore[misc] +) def status( - file_id: str = typer.Argument(..., help="ID of the file to check the status of"), + file_id: str = typer.Argument(help="ID of the file to check the status of"), ) -> None: with console.status(f"Monitoring status of file {file_id}...") as status: while True: @@ -114,9 +117,7 @@ def status( @app.command( help="List the files on OpenAI's servers", -) # type: ignore[misc] -def list( - limit: int = typer.Option(5, help="Limit the number of files to list"), -) -> None: - files = get_files(limit=limit) +) +def list() -> None: + files = get_files() console.log(generate_file_table(files)) diff --git a/instructor/cli/hub.py b/instructor/cli/hub.py index abd804e9d..d0eddb6ef 100644 --- a/instructor/cli/hub.py +++ b/instructor/cli/hub.py @@ -58,7 +58,7 @@ def get_cookbooks(self, branch: str, q: Optional[str] = None, sort: bool = False else: raise Exception(f"Failed to fetch cookbooks: {response.status_code}") - def get_content_markdown(self, branch, slug): + def get_content_markdown(self, branch: str, slug: str) -> str: """Get markdown content.""" url = f"{self.base_url}/api/{branch}/items/{slug}/md/" response = httpx.get(url) @@ -67,7 +67,7 @@ def get_content_markdown(self, branch, slug): else: raise Exception(f"Failed to fetch markdown content: {response.status_code}") - def get_content_python(self, branch, slug): + def get_content_python(self, branch: str, slug: str) -> str: """Get Python code blocks from content.""" url = f"{self.base_url}/api/{branch}/items/{slug}/py/" response = httpx.get(url) @@ -76,12 +76,12 @@ def get_content_python(self, branch, slug): else: raise Exception(f"Failed to fetch Python content: {response.status_code}") - def get_cookbook_id(self, id: int, branch: str = "main") -> HubPage: + def get_cookbook_id(self, id: int, branch: str = "main") -> Optional[HubPage]: for cookbook in self.get_cookbooks(branch): if cookbook.id == id: return cookbook - def get_cookbook_slug(self, slug: str, branch: str = "main") -> HubPage: + def get_cookbook_slug(self, slug: str, branch: str = "main") -> Optional[HubPage]: for cookbook in self.get_cookbooks(branch): if cookbook.slug == slug: return cookbook @@ -155,7 +155,7 @@ def pull( if file: with open(file, "w") as f: - f.write(output) + f.write(output) # type: ignore - markdown is writable return if page: diff --git a/instructor/cli/jobs.py b/instructor/cli/jobs.py index 94d8acfc8..9a2c292ed 100644 --- a/instructor/cli/jobs.py +++ b/instructor/cli/jobs.py @@ -1,13 +1,13 @@ -from typing import Dict, List, Union +from typing import Optional, TypedDict from openai import OpenAI +from openai.types.fine_tuning.job_create_params import Hyperparameters import typer import time from rich.live import Live from rich.table import Table from rich.console import Console from datetime import datetime -from typing import cast from openai.types.fine_tuning import FineTuningJob client = OpenAI() @@ -15,10 +15,15 @@ console = Console() -def generate_table(jobs: List[FineTuningJob]) -> Table: +class FuneTuningParams(TypedDict, total=False): + hyperparameters: Hyperparameters + validation_file: Optional[str] + suffix: Optional[str] + + +def generate_table(jobs: list[FineTuningJob]) -> Table: # Sorting the jobs by creation time - jobs = sorted(jobs, key=lambda x: (cast(FineTuningJob, x)).created_at, reverse=True) - jobs = cast(List[FineTuningJob], jobs) + jobs = sorted(jobs, key=lambda x: x.created_at, reverse=True) table = Table( title="OpenAI Fine Tuning Job Monitoring", @@ -66,7 +71,7 @@ def status_color(status: str) -> str: ) -def get_jobs(limit: int = 5) -> List[FineTuningJob]: +def get_jobs(limit: int = 5) -> list[FineTuningJob]: return client.fine_tuning.jobs.list(limit=limit).data @@ -78,7 +83,7 @@ def get_file_status(file_id: str) -> str: @app.command( name="list", help="Monitor the status of the most recent fine-tuning jobs.", -) # type: ignore[misc] +) def watch( limit: int = typer.Option(5, help="Limit the number of jobs to monitor"), poll: int = typer.Option(5, help="Polling interval in seconds"), @@ -97,24 +102,24 @@ def watch( @app.command( help="Create a fine-tuning job from an existing ID.", -) # type: ignore[misc] +) def create_from_id( - id: str = typer.Argument(..., help="ID of the existing fine-tuning job"), + id: str = typer.Argument(help="ID of the existing fine-tuning job"), model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"), - n_epochs: int = typer.Option( + n_epochs: Optional[int] = typer.Option( None, help="Number of epochs for fine-tuning", show_default=False ), - batch_size: int = typer.Option( + batch_size: Optional[int] = typer.Option( None, help="Batch size for fine-tuning", show_default=False ), - learning_rate_multiplier: float = typer.Option( + learning_rate_multiplier: Optional[float] = typer.Option( None, help="Learning rate multiplier for fine-tuning", show_default=False ), - validation_file_id: str = typer.Option( + validation_file_id: Optional[str] = typer.Option( None, help="ID of the uploaded validation file" ), ) -> None: - hyperparameters_dict: Dict[str, Union[int, float, str]] = {} + hyperparameters_dict: Hyperparameters = {} if n_epochs is not None: hyperparameters_dict["n_epochs"] = n_epochs if batch_size is not None: @@ -128,7 +133,7 @@ def create_from_id( job = client.fine_tuning.jobs.create( training_file=id, model=model, - hyperparameters=hyperparameters_dict if hyperparameters_dict else None, + hyperparameters=hyperparameters_dict, validation_file=validation_file_id if validation_file_id else None, ) console.log(f"[bold green]Fine-tuning job created with ID: {job.id}") @@ -137,24 +142,28 @@ def create_from_id( @app.command( help="Create a fine-tuning job from a file.", -) # type: ignore[misc] +) def create_from_file( - file: str = typer.Argument(..., help="Path to the file for fine-tuning"), + file: str = typer.Argument(help="Path to the file for fine-tuning"), model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"), poll: int = typer.Option(2, help="Polling interval in seconds"), - n_epochs: int = typer.Option( + n_epochs: Optional[int] = typer.Option( None, help="Number of epochs for fine-tuning", show_default=False ), - batch_size: int = typer.Option( + batch_size: Optional[int] = typer.Option( None, help="Batch size for fine-tuning", show_default=False ), - learning_rate_multiplier: float = typer.Option( + learning_rate_multiplier: Optional[float] = typer.Option( None, help="Learning rate multiplier for fine-tuning", show_default=False ), - validation_file: str = typer.Option(None, help="Path to the validation file"), - model_suffix: str = typer.Option(None, help="Suffix to identify the model"), + validation_file: Optional[str] = typer.Option( + None, help="Path to the validation file" + ), + model_suffix: Optional[str] = typer.Option( + None, help="Suffix to identify the model" + ), ) -> None: - hyperparameters_dict: Dict[str, Union[int, float, str]] = {} + hyperparameters_dict: Hyperparameters = {} if n_epochs is not None: hyperparameters_dict["n_epochs"] = n_epochs if batch_size is not None: @@ -177,8 +186,9 @@ def create_from_file( status.spinner_style = "dots" while True: file_status = get_file_status(file_id) - if validation_file_id: - validation_file_status = get_file_status(validation_file_id) + validation_file_status = ( + get_file_status(validation_file_id) if validation_file_id else "" + ) if file_status == "processed" and ( not validation_file_id or validation_file_status == "processed" @@ -192,7 +202,7 @@ def create_from_file( time.sleep(poll) - additional_params: Dict[str, Union[str, Dict[str, Union[int, float, str]]]] = {} + additional_params: FuneTuningParams = {} if hyperparameters_dict: additional_params["hyperparameters"] = hyperparameters_dict if validation_file: @@ -218,9 +228,9 @@ def create_from_file( @app.command( help="Cancel a fine-tuning job.", -) # type: ignore[misc] +) def cancel( - id: str = typer.Argument(..., help="ID of the fine-tuning job to cancel"), + id: str = typer.Argument(help="ID of the fine-tuning job to cancel"), ) -> None: with console.status(f"[bold red]Cancelling job {id}...", spinner="dots"): try: diff --git a/instructor/cli/usage.py b/instructor/cli/usage.py index 87d2e63ea..ce8db225b 100644 --- a/instructor/cli/usage.py +++ b/instructor/cli/usage.py @@ -1,9 +1,11 @@ -from typing import List, Dict, Any, Union, DefaultDict +from typing import Any, Union +from collections.abc import Awaitable from datetime import datetime, timedelta import typer import os import aiohttp import asyncio +from builtins import list as List from collections import defaultdict from rich.console import Console from rich.table import Table @@ -18,7 +20,7 @@ api_key = os.environ.get("OPENAI_API_KEY") -async def fetch_usage(date: str) -> Dict[str, Any]: +async def fetch_usage(date: str) -> dict[str, Any]: headers = {"Authorization": f"Bearer {api_key}"} url = f"https://api.openai.com/v1/usage?date={date}" async with aiohttp.ClientSession() as session: @@ -26,9 +28,9 @@ async def fetch_usage(date: str) -> Dict[str, Any]: return await resp.json() -async def get_usage_for_past_n_days(n_days: int) -> List[Dict[str, Any]]: - tasks = [] - all_data = [] +async def get_usage_for_past_n_days(n_days: int) -> list[dict[str, Any]]: + tasks: List[Awaitable[dict[str, Any]]] = [] # noqa: UP006 - conflicting with the fn name + all_data: List[dict[str, Any]] = [] # noqa: UP006 - conflicting with the fn name with Progress() as progress: if n_days > 1: task = progress.add_task("[green]Fetching usage data...", total=n_days) @@ -46,44 +48,39 @@ async def get_usage_for_past_n_days(n_days: int) -> List[Dict[str, Any]]: # Define the cost per unit for each model -# Add temporary body type hint here because mypy may infer the dict type -# from the first few items (?) in the dict, which may not be representative of -# the entire dict. -MODEL_COSTS: Dict[ - ModelNames, - Union[Dict[str, float], float], -] = { - "gpt-4-0125-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, - "gpt-4-turbo-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, - "gpt-4-1106-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, - "gpt-4-vision-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, - "gpt-4": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, - "gpt-4-0314": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, - "gpt-4-0613": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, - "gpt-4-32k": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, - "gpt-4-32k-0314": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, - "gpt-4-32k-0613": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, - "gpt-3.5-turbo": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000}, - "gpt-3.5-turbo-16k": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000}, - "gpt-3.5-turbo-0301": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, - "gpt-3.5-turbo-0613": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, - "gpt-3.5-turbo-1106": {"prompt": 0.0010 / 1000, "completion": 0.0020 / 1000}, - "gpt-3.5-turbo-0125": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000}, - "gpt-3.5-turbo-16k-0613": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000}, - "gpt-3.5-turbo-instruct": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, - "text-embedding-3-small": 0.00002 / 1000, - "text-embedding-3-large": 0.00013 / 1000, - "text-embedding-ada-002": 0.00010 / 1000, +MODEL_COSTS = { + "gpt-4o": {"prompt": 0.005 / 1000, "completion": 0.015 / 1000}, + "gpt-4-0125-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, + "gpt-4-turbo-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, + "gpt-4-1106-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, + "gpt-4-vision-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000}, + "gpt-4": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, + "gpt-4-0314": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, + "gpt-4-0613": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000}, + "gpt-4-32k": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, + "gpt-4-32k-0314": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, + "gpt-4-32k-0613": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000}, + "gpt-3.5-turbo": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000}, + "gpt-3.5-turbo-16k": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000}, + "gpt-3.5-turbo-0301": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, + "gpt-3.5-turbo-0613": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, + "gpt-3.5-turbo-1106": {"prompt": 0.0010 / 1000, "completion": 0.0020 / 1000}, + "gpt-3.5-turbo-0125": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000}, + "gpt-3.5-turbo-16k-0613": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000}, + "gpt-3.5-turbo-instruct": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000}, + "text-embedding-3-small": 0.00002 / 1000, + "text-embedding-3-large": 0.00013 / 1000, + "text-embedding-ada-002": 0.00010 / 1000, } def get_model_cost( model: ModelNames, -) -> Union[Dict[str, float], float]: +) -> Union[dict[str, float], float]: """Get the cost details for a given model.""" if model in MODEL_COSTS: return MODEL_COSTS[model] - + if model.startswith("gpt-3.5-turbo-16k"): return MODEL_COSTS["gpt-3.5-turbo-16k"] elif model.startswith("gpt-3.5-turbo"): @@ -104,7 +101,7 @@ def calculate_cost( """Calculate the cost based on the snapshot ID and number of tokens.""" cost = get_model_cost(snapshot_id) - if isinstance(cost, float): + if isinstance(cost, (float, int)): return cost * (n_context_tokens + n_generated_tokens) prompt_cost = cost["prompt"] * n_context_tokens @@ -112,13 +109,13 @@ def calculate_cost( return prompt_cost + completion_cost -def group_and_sum_by_date_and_snapshot(usage_data: List[Dict[str, Any]]) -> Table: +def group_and_sum_by_date_and_snapshot(usage_data: list[dict[str, Any]]) -> Table: """Group and sum the usage data by date and snapshot, including costs.""" - summary: DefaultDict[ - str, DefaultDict[str, Dict[str, Union[int, float]]] - ] = defaultdict( - lambda: defaultdict( - lambda: {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0} + summary: defaultdict[str, defaultdict[str, dict[str, Union[int, float]]]] = ( + defaultdict( + lambda: defaultdict( + lambda: {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0} + ) ) ) @@ -160,7 +157,7 @@ def group_and_sum_by_date_and_snapshot(usage_data: List[Dict[str, Any]]) -> Tabl return table -@app.command(help="Displays OpenAI API usage data for the past N days.") # type: ignore[misc] +@app.command(help="Displays OpenAI API usage data for the past N days.") def list( n: int = typer.Option(0, help="Number of days."), ) -> None: diff --git a/instructor/client.py b/instructor/client.py new file mode 100644 index 000000000..b21916ddd --- /dev/null +++ b/instructor/client.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +import openai +import inspect +import instructor +from .utils import Provider, get_provider +from openai.types.chat import ChatCompletionMessageParam +from typing import ( + TypeVar, + Callable, + overload, + Union, + Any, +) +from collections.abc import Generator, Iterable, Awaitable, AsyncGenerator +from typing_extensions import Self +from pydantic import BaseModel +from instructor.dsl.partial import Partial + + +T = TypeVar("T", bound=Union[BaseModel, "Iterable[Any]", "Partial[Any]"]) + + +class Instructor: + client: Any | None + create_fn: Callable[..., Any] + mode: instructor.Mode + default_model: str | None = None + provider: Provider + + def __init__( + self, + client: Any | None, + create: Callable[..., Any], + mode: instructor.Mode = instructor.Mode.TOOLS, + provider: Provider = Provider.OPENAI, + **kwargs: Any, + ): + self.client = client + self.create_fn = create + self.mode = mode + self.kwargs = kwargs + self.provider = provider + + @property + def chat(self) -> Self: + return self + + @property + def completions(self) -> Self: + return self + + @property + def messages(self) -> Self: + return self + + @overload + def create( + self: AsyncInstructor, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Awaitable[T]: ... + + @overload + def create( + self: Self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> T: ... + + # TODO: we should overload a case where response_model is None + def create( + self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> T | Awaitable[T]: + kwargs = self.handle_kwargs(kwargs) + + return self.create_fn( + response_model=response_model, + messages=messages, + max_retries=max_retries, + validation_context=validation_context, + strict=strict, + **kwargs, + ) + + @overload + def create_partial( + self: AsyncInstructor, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> AsyncGenerator[T, None]: ... + + @overload + def create_partial( + self: Self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Generator[T, None, None]: ... + + def create_partial( + self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Generator[T, None, None] | AsyncGenerator[T, None]: + assert self.provider != Provider.ANTHROPIC, "Anthropic doesn't support partial" + + kwargs["stream"] = True + + kwargs = self.handle_kwargs(kwargs) + + response_model = instructor.Partial[response_model] # type: ignore + return self.create_fn( + messages=messages, + response_model=response_model, + max_retries=max_retries, + validation_context=validation_context, + strict=strict, + **kwargs, + ) + + @overload + def create_iterable( + self: AsyncInstructor, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> AsyncGenerator[T, None]: ... + + @overload + def create_iterable( + self: Self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Generator[T, None, None]: ... + + def create_iterable( + self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Generator[T, None, None] | AsyncGenerator[T, None]: + assert self.provider != Provider.ANTHROPIC, "Anthropic doesn't support iterable" + + kwargs["stream"] = True + kwargs = self.handle_kwargs(kwargs) + + response_model = Iterable[response_model] # type: ignore + return self.create_fn( + messages=messages, + response_model=response_model, + max_retries=max_retries, + validation_context=validation_context, + strict=strict, + **kwargs, + ) + + @overload + def create_with_completion( + self: AsyncInstructor, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> Awaitable[tuple[T, Any]]: ... + + @overload + def create_with_completion( + self: Self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> tuple[T, Any]: ... + + def create_with_completion( + self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> tuple[T, Any] | Awaitable[tuple[T, Any]]: + kwargs = self.handle_kwargs(kwargs) + model = self.create_fn( + messages=messages, + response_model=response_model, + max_retries=max_retries, + validation_context=validation_context, + strict=strict, + **kwargs, + ) + return model, model._raw_response + + def handle_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]: + for key, value in self.kwargs.items(): + if key not in kwargs: + kwargs[key] = value + return kwargs + + +class AsyncInstructor(Instructor): + client: Any | None + create_fn: Callable[..., Any] + mode: instructor.Mode + default_model: str | None = None + provider: Provider + + def __init__( + self, + client: Any | None, + create: Callable[..., Any], + mode: instructor.Mode = instructor.Mode.TOOLS, + provider: Provider = Provider.OPENAI, + **kwargs: Any, + ): + self.client = client + self.create_fn = create + self.mode = mode + self.kwargs = kwargs + self.provider = provider + + async def create( + self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> T: + kwargs = self.handle_kwargs(kwargs) + return await self.create_fn( + response_model=response_model, + validation_context=validation_context, + max_retries=max_retries, + messages=messages, + strict=strict, + **kwargs, + ) + + async def create_partial( + self, + response_model: type[T], + messages: list[ChatCompletionMessageParam], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> AsyncGenerator[T, None]: + assert self.provider != Provider.ANTHROPIC, "Anthropic doesn't support partial" + + kwargs = self.handle_kwargs(kwargs) + kwargs["stream"] = True + async for item in await self.create_fn( + response_model=instructor.Partial[response_model], # type: ignore + validation_context=validation_context, + max_retries=max_retries, + messages=messages, + strict=strict, + **kwargs, + ): + yield item + + async def create_iterable( + self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> AsyncGenerator[T, None]: + assert self.provider != Provider.ANTHROPIC, "Anthropic doesn't support iterable" + + kwargs = self.handle_kwargs(kwargs) + kwargs["stream"] = True + async for item in await self.create_fn( + response_model=Iterable[response_model], + validation_context=validation_context, + max_retries=max_retries, + messages=messages, + strict=strict, + **kwargs, + ): + yield item + + async def create_with_completion( + self, + messages: list[ChatCompletionMessageParam], + response_model: type[T], + max_retries: int = 3, + validation_context: dict[str, Any] | None = None, + strict: bool = True, + **kwargs: Any, + ) -> tuple[T, Any]: + kwargs = self.handle_kwargs(kwargs) + response = await self.create_fn( + response_model=response_model, + validation_context=validation_context, + max_retries=max_retries, + messages=messages, + strict=strict, + **kwargs, + ) + return response, response._raw_response + + +@overload +def from_openai( + client: openai.OpenAI, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> Instructor: + pass + + +@overload +def from_openai( + client: openai.AsyncOpenAI, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> AsyncInstructor: + pass + + +def from_openai( + client: openai.OpenAI | openai.AsyncOpenAI, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> Instructor | AsyncInstructor: + if hasattr(client, "base_url"): + provider = get_provider(str(client.base_url)) + else: + provider = Provider.OPENAI + + if not isinstance(client, (openai.OpenAI, openai.AsyncOpenAI)): + import warnings + + warnings.warn( + "Client should be an instance of openai.OpenAI or openai.AsyncOpenAI. Unexpected behavior may occur with other client types.", + stacklevel=2, + ) + + if provider in {Provider.ANYSCALE, Provider.TOGETHER}: + assert mode in { + instructor.Mode.TOOLS, + instructor.Mode.JSON, + instructor.Mode.JSON_SCHEMA, + instructor.Mode.MD_JSON, + } + + if provider in {Provider.OPENAI}: + assert mode in { + instructor.Mode.TOOLS, + instructor.Mode.JSON, + instructor.Mode.FUNCTIONS, + instructor.Mode.PARALLEL_TOOLS, + instructor.Mode.MD_JSON, + } + + if isinstance(client, openai.OpenAI): + return Instructor( + client=client, + create=instructor.patch(create=client.chat.completions.create, mode=mode), + mode=mode, + provider=provider, + **kwargs, + ) + + if isinstance(client, openai.AsyncOpenAI): + return AsyncInstructor( + client=client, + create=instructor.patch(create=client.chat.completions.create, mode=mode), + mode=mode, + provider=provider, + **kwargs, + ) + + +@overload +def from_litellm( + completion: Callable[..., Any], + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> Instructor: ... + + +@overload +def from_litellm( + completion: Awaitable[Any], + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> AsyncInstructor: + pass + + +def from_litellm( + completion: Callable[..., Any] | Awaitable[Any], + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> Instructor | AsyncInstructor: + is_async = inspect.isawaitable(completion) + + if not is_async: + return Instructor( + client=None, + create=instructor.patch(create=completion, mode=mode), + mode=mode, + **kwargs, + ) + else: + return AsyncInstructor( + client=None, + create=instructor.patch(create=completion, mode=mode), + mode=mode, + **kwargs, + ) diff --git a/instructor/client_anthropic.py b/instructor/client_anthropic.py new file mode 100644 index 000000000..f2900dc55 --- /dev/null +++ b/instructor/client_anthropic.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import anthropic +import instructor + +from typing import overload, Any + + +@overload +def from_anthropic( + client: ( + anthropic.Anthropic | anthropic.AnthropicBedrock | anthropic.AnthropicVertex + ), + mode: instructor.Mode = instructor.Mode.ANTHROPIC_JSON, + **kwargs: Any, +) -> instructor.Instructor: ... + + +@overload +def from_anthropic( + client: ( + anthropic.AsyncAnthropic + | anthropic.AsyncAnthropicBedrock + | anthropic.AsyncAnthropicVertex + ), + mode: instructor.Mode = instructor.Mode.ANTHROPIC_JSON, + **kwargs: Any, +) -> instructor.AsyncInstructor: ... + + +def from_anthropic( + client: ( + anthropic.Anthropic + | anthropic.AsyncAnthropic + | anthropic.AnthropicBedrock + | anthropic.AsyncAnthropicBedrock + | anthropic.AsyncAnthropicVertex + | anthropic.AnthropicVertex + ), + mode: instructor.Mode = instructor.Mode.ANTHROPIC_JSON, + **kwargs: Any, +) -> instructor.Instructor | instructor.AsyncInstructor: + assert ( + mode + in { + instructor.Mode.ANTHROPIC_JSON, + instructor.Mode.ANTHROPIC_TOOLS, + } + ), "Mode be one of {instructor.Mode.ANTHROPIC_JSON, instructor.Mode.ANTHROPIC_TOOLS}" + + assert isinstance( + client, + ( + anthropic.Anthropic, + anthropic.AsyncAnthropic, + anthropic.AnthropicBedrock, + anthropic.AnthropicVertex, + anthropic.AsyncAnthropicBedrock, + anthropic.AsyncAnthropicVertex, + ), + ), "Client must be an instance of {anthropic.Anthropic, anthropic.AsyncAnthropic, anthropic.AnthropicBedrock, anthropic.AsyncAnthropicBedrock, anthropic.AnthropicVertex, anthropic.AsyncAnthropicVertex}" + + if mode == instructor.Mode.ANTHROPIC_TOOLS: + create = client.beta.tools.messages.create # type: ignore - unknown in stubs + else: + create = client.messages.create + + if isinstance( + client, + (anthropic.Anthropic, anthropic.AnthropicBedrock, anthropic.AnthropicVertex), + ): + return instructor.Instructor( + client=client, + create=instructor.patch(create=create, mode=mode), + provider=instructor.Provider.ANTHROPIC, + mode=mode, + **kwargs, + ) + + else: + return instructor.AsyncInstructor( + client=client, + create=instructor.patch(create=create, mode=mode), + provider=instructor.Provider.ANTHROPIC, + mode=mode, + **kwargs, + ) diff --git a/instructor/client_cohere.py b/instructor/client_cohere.py new file mode 100644 index 000000000..d823b870c --- /dev/null +++ b/instructor/client_cohere.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import cohere +import instructor +from functools import wraps +from typing import ( + TypeVar, + overload, +) +from typing import Any +from typing_extensions import ParamSpec +from pydantic import BaseModel +from instructor.process_response import handle_response_model +from instructor.retry import retry_async + + +T_Model = TypeVar("T_Model", bound=BaseModel) +T_ParamSpec = ParamSpec("T_ParamSpec") + + +@overload +def from_cohere( + client: cohere.Client, + mode: instructor.Mode = instructor.Mode.COHERE_TOOLS, + **kwargs: Any, +) -> instructor.Instructor: ... + + +@overload +def from_cohere( + client: cohere.AsyncClient, + mode: instructor.Mode = instructor.Mode.COHERE_TOOLS, + **kwargs: Any, +) -> instructor.AsyncInstructor: ... + + +def from_cohere( + client: cohere.Client | cohere.AsyncClient, + mode: instructor.Mode = instructor.Mode.COHERE_TOOLS, + **kwargs: Any, +): + assert mode in { + instructor.Mode.COHERE_TOOLS, + }, "Mode be one of {instructor.Mode.COHERE_TOOLS}" + + assert isinstance( + client, (cohere.Client, cohere.AsyncClient) + ), "Client must be an instance of cohere.Cohere or cohere.AsyncCohere" + + if isinstance(client, cohere.Client): + return instructor.Instructor( + client=client, + create=instructor.patch(create=client.chat, mode=mode), + provider=instructor.Provider.COHERE, + mode=mode, + **kwargs, + ) + + @wraps(client.chat) + async def new_create_async( + response_model: type[T_Model] | None = None, + validation_context: dict[str, Any] | None = None, + max_retries: int = 1, + *args: T_ParamSpec.args, + **kwargs: T_ParamSpec.kwargs, + ) -> T_Model: + prepared_response_model, new_kwargs = handle_response_model( + response_model=response_model, + mode=mode, + **kwargs, + ) + response = await retry_async( + func=client.chat, + response_model=prepared_response_model, + validation_context=validation_context, + max_retries=max_retries, + args=args, + kwargs=new_kwargs, + mode=mode, + ) + return response + + return instructor.AsyncInstructor( + client=client, + create=new_create_async, + provider=instructor.Provider.COHERE, + mode=mode, + **kwargs, + ) diff --git a/instructor/client_groq.py b/instructor/client_groq.py new file mode 100644 index 000000000..aeb25f83b --- /dev/null +++ b/instructor/client_groq.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import overload, Any + +import groq +import instructor + + +@overload +def from_groq( + client: groq.Groq, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> instructor.Instructor: ... + + +@overload +def from_groq( + client: groq.AsyncGroq, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> instructor.Instructor: ... + + +def from_groq( + client: groq.Groq | groq.AsyncGroq, + mode: instructor.Mode = instructor.Mode.TOOLS, + **kwargs: Any, +) -> instructor.Instructor: + assert mode in { + instructor.Mode.JSON, + instructor.Mode.TOOLS, + }, "Mode be one of {instructor.Mode.JSON, instructor.Mode.TOOLS}" + + assert isinstance( + client, (groq.Groq, groq.AsyncGroq) + ), "Client must be an instance of groq.GROQ" + + if isinstance(client, groq.Groq): + return instructor.Instructor( + client=client, + create=instructor.patch(create=client.chat.completions.create, mode=mode), + provider=instructor.Provider.GROQ, + mode=mode, + **kwargs, + ) + + else: + return instructor.Instructor( + client=client, + create=instructor.patch(create=client.chat.completions.create, mode=mode), + provider=instructor.Provider.GROQ, + mode=mode, + **kwargs, + ) diff --git a/instructor/client_mistral.py b/instructor/client_mistral.py new file mode 100644 index 000000000..a586dccbb --- /dev/null +++ b/instructor/client_mistral.py @@ -0,0 +1,55 @@ +# Future imports to ensure compatibility with Python 3.9 +from __future__ import annotations + +import mistralai.client +import mistralai.async_client as mistralaiasynccli +import instructor +from typing import overload, Any + + +@overload +def from_mistral( + client: mistralai.client.MistralClient, + mode: instructor.Mode = instructor.Mode.MISTRAL_TOOLS, + **kwargs: Any, +) -> instructor.Instructor: ... + + +@overload +def from_mistral( + client: mistralaiasynccli.MistralAsyncClient, + mode: instructor.Mode = instructor.Mode.MISTRAL_TOOLS, + **kwargs: Any, +) -> instructor.AsyncInstructor: ... + + +def from_mistral( + client: mistralai.client.MistralClient | mistralaiasynccli.MistralAsyncClient, + mode: instructor.Mode = instructor.Mode.MISTRAL_TOOLS, + **kwargs: Any, +) -> instructor.Instructor | instructor.AsyncInstructor: + assert mode in { + instructor.Mode.MISTRAL_TOOLS, + }, "Mode be one of {instructor.Mode.MISTRAL_TOOLS}" + + assert isinstance( + client, (mistralai.client.MistralClient, mistralaiasynccli.MistralAsyncClient) + ), "Client must be an instance of mistralai.client.MistralClient or mistralai.async_cli.MistralAsyncClient" + + if isinstance(client, mistralai.client.MistralClient): + return instructor.Instructor( + client=client, + create=instructor.patch(create=client.chat, mode=mode), + provider=instructor.Provider.MISTRAL, + mode=mode, + **kwargs, + ) + + else: + return instructor.AsyncInstructor( + client=client, + create=instructor.patch(create=client.chat, mode=mode), + provider=instructor.Provider.MISTRAL, + mode=mode, + **kwargs, + ) diff --git a/instructor/distil.py b/instructor/distil.py index d7fb22aa2..16305ffdc 100644 --- a/instructor/distil.py +++ b/instructor/distil.py @@ -5,19 +5,36 @@ import inspect import functools -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar +from typing import ( + Any, + Callable, + Optional, + TypeVar, + TypedDict, + Literal, + Union, +) +from typing_extensions import ParamSpec, NotRequired +from openai.types.chat.chat_completion import ChatCompletion +from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam from pydantic import BaseModel, validate_call from openai import OpenAI from instructor.function_calls import openai_schema -T_Retval = TypeVar("T_Retval") +P = ParamSpec("P") +T_Retval = TypeVar("T_Retval", bound=BaseModel) + + +class OpenAIChatKwargs(TypedDict): + messages: list[ChatCompletionMessageParam] + functions: NotRequired[list[dict[str, Any]]] class FinetuneFormat(enum.Enum): - MESSAGES: str = "messages" - RAW: str = "raw" + MESSAGES = "messages" + RAW = "raw" def get_signature_from_fn(fn: Callable[..., Any]) -> str: @@ -45,7 +62,7 @@ def get_signature_from_fn(fn: Callable[..., Any]) -> str: return f"{lines}\n{formatted_docstring}" -@functools.lru_cache() +@functools.lru_cache def format_function(func: Callable[..., Any]) -> str: """ Format a function as a string with docstring and body. @@ -84,7 +101,7 @@ def __init__( self, name: Optional[str] = None, id: Optional[str] = None, - log_handlers: Optional[List[logging.Handler]] = None, + log_handlers: Optional[list[logging.Handler]] = None, finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES, indent: int = 2, include_code_body: bool = False, @@ -116,12 +133,12 @@ def distil( self, *args: Any, name: Optional[str] = None, - mode: str = "distil", + mode: Literal["distil", "dispatch"] = "distil", model: str = "gpt-3.5-turbo", fine_tune_format: Optional[FinetuneFormat] = None, - ) -> Callable[ - [Callable[..., Any]], - Callable[[Callable[..., T_Retval]], Callable[..., T_Retval]], + ) -> Union[ + Callable[P, Union[T_Retval, ChatCompletion]], + Callable[[Callable[P, T_Retval]], Callable[P, Union[T_Retval, ChatCompletion]]], ]: """ Decorator to track the function call and response, supports distillation and dispatch modes. @@ -149,33 +166,38 @@ def distil( fine_tune_format = self.finetune_format def _wrap_distil( - fn: Callable[..., Any], - ) -> Callable[[Callable[..., T_Retval]], Callable[..., T_Retval]]: + fn: Callable[P, T_Retval], + ) -> Callable[P, Union[T_Retval, ChatCompletion]]: msg = f"Return type hint for {fn} must subclass `pydantic.BaseModel'" assert is_return_type_base_model_or_instance(fn), msg return_base_model = inspect.signature(fn).return_annotation @functools.wraps(fn) - def _dispatch(*args: Any, **kwargs: Any) -> Callable[..., T_Retval]: - name = name if name else fn.__name__ + def _dispatch(*args: P.args, **kwargs: P.kwargs) -> ChatCompletion: openai_kwargs = self.openai_kwargs( - name=name, + name=name if name else fn.__name__, fn=fn, args=args, kwargs=kwargs, base_model=return_base_model, ) return self.client.chat.completions.create( - **openai_kwargs, model=model, response_model=return_base_model + **openai_kwargs, + model=model, + response_model=return_base_model, # type: ignore - TODO figure out why `response_model` is not recognized ) @functools.wraps(fn) - def _distil(*args: Any, **kwargs: Any) -> Callable[..., T_Retval]: + def _distil(*args: P.args, **kwargs: P.kwargs) -> T_Retval: resp = fn(*args, **kwargs) self.track( - fn, args, kwargs, resp, name=name, finetune_format=fine_tune_format + fn, + args, + kwargs, + resp, + name=name, + finetune_format=fine_tune_format, ) - return resp return _dispatch if mode == "dispatch" else _distil @@ -185,12 +207,12 @@ def _distil(*args: Any, **kwargs: Any) -> Callable[..., T_Retval]: return _wrap_distil - @validate_call # type: ignore[misc] + @validate_call def track( self, fn: Callable[..., Any], - args: Tuple[Any, ...], - kwargs: Dict[str, Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], resp: BaseModel, name: Optional[str] = None, finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES, @@ -206,7 +228,7 @@ def track( :param finetune_format: Format to use for finetuning. Defaults to "raw". """ name = name if name else fn.__name__ - base_model: BaseModel = type(resp) + base_model = type(resp) if finetune_format == FinetuneFormat.MESSAGES: openai_function_call = openai_schema(base_model).openai_schema @@ -238,10 +260,10 @@ def openai_kwargs( self, name: str, fn: Callable[..., Any], - args: Tuple[Any, ...], - kwargs: Dict[str, Any], - base_model: Type[BaseModel], - ) -> Dict[str, Any]: + args: tuple[Any, ...], + kwargs: dict[str, Any], + base_model: type[BaseModel], + ) -> OpenAIChatKwargs: if self.include_code_body: func_def = format_function(fn) else: @@ -253,7 +275,7 @@ def openai_kwargs( ) call_args = ", ".join(filter(None, [str_args, str_kwargs])) - function_body = { + function_body: OpenAIChatKwargs = { "messages": [ { "role": "system", diff --git a/instructor/dsl/citation.py b/instructor/dsl/citation.py index df72b2493..c5ccd4f04 100644 --- a/instructor/dsl/citation.py +++ b/instructor/dsl/citation.py @@ -1,8 +1,8 @@ -from pydantic import BaseModel, Field, FieldValidationInfo, model_validator -from typing import Generator, List, Tuple +from pydantic import BaseModel, Field, model_validator, ValidationInfo +from collections.abc import Generator -class CitationMixin(BaseModel): # type: ignore[misc] +class CitationMixin(BaseModel): """ Helpful mixing that can use `validation_context={"context": context}` in `from_response` to find the span of the substring_phrase in the context. @@ -53,12 +53,12 @@ class User(BaseModel): """ - substring_quotes: List[str] = Field( + substring_quotes: list[str] = Field( description="List of unique and specific substrings of the quote that was used to answer the question.", ) @model_validator(mode="after") # type: ignore[misc] - def validate_sources(self, info: FieldValidationInfo) -> "CitationMixin": + def validate_sources(self, info: ValidationInfo) -> "CitationMixin": """ For each substring_phrase, find the span of the substring_phrase in the context. If the span is not found, remove the substring_phrase from the list. @@ -77,8 +77,8 @@ def validate_sources(self, info: FieldValidationInfo) -> "CitationMixin": def _get_span( self, quote: str, context: str, errs: int = 5 - ) -> Generator[Tuple[int, int], None, None]: - import regex # type: ignore[import-untyped] + ) -> Generator[tuple[int, int], None, None]: + import regex minor = quote major = context @@ -92,6 +92,6 @@ def _get_span( if s is not None: yield from s.spans() - def get_spans(self, context: str) -> Generator[Tuple[int, int], None, None]: + def get_spans(self, context: str) -> Generator[tuple[int, int], None, None]: for quote in self.substring_quotes: yield from self._get_span(quote, context) diff --git a/instructor/dsl/iterable.py b/instructor/dsl/iterable.py index fd2c9e2cb..8d325d7ad 100644 --- a/instructor/dsl/iterable.py +++ b/instructor/dsl/iterable.py @@ -1,18 +1,25 @@ -from typing import Any, AsyncGenerator, Generator, Iterable, List, Optional, Tuple, Type +from typing import Any, Optional, cast, ClassVar +from collections.abc import AsyncGenerator, Generator, Iterable -from pydantic import BaseModel, Field, create_model +from pydantic import BaseModel, Field, create_model # type: ignore - remove once Pydantic is updated -from instructor.function_calls import OpenAISchema, Mode +from instructor.function_calls import OpenAISchema +from instructor.mode import Mode +from instructor.utils import extract_json_from_stream, extract_json_from_stream_async class IterableBase: - task_type = None # type: ignore[var-annotated] + task_type: ClassVar[Optional[type[BaseModel]]] = None @classmethod def from_streaming_response( cls, completion: Iterable[Any], mode: Mode, **kwargs: Any ) -> Generator[BaseModel, None, None]: # noqa: ARG003 json_chunks = cls.extract_json(completion, mode) + + if mode == Mode.MD_JSON: + json_chunks = extract_json_from_stream(json_chunks) + yield from cls.tasks_from_chunks(json_chunks, **kwargs) @classmethod @@ -20,6 +27,10 @@ async def from_streaming_response_async( cls, completion: AsyncGenerator[Any, None], mode: Mode, **kwargs: Any ) -> AsyncGenerator[BaseModel, None]: json_chunks = cls.extract_json_async(completion, mode) + + if mode == Mode.MD_JSON: + json_chunks = extract_json_from_stream_async(json_chunks) + return cls.tasks_from_chunks_async(json_chunks, **kwargs) @classmethod @@ -109,22 +120,23 @@ async def extract_json_async( pass @staticmethod - def get_object(s: str, stack: int) -> Tuple[Optional[str], str]: + def get_object(s: str, stack: int) -> tuple[Optional[str], str]: + start_index = s.find("{") for i, c in enumerate(s): if c == "{": stack += 1 if c == "}": stack -= 1 if stack == 0: - return s[: i + 1], s[i + 2 :] + return s[start_index : i + 1], s[i + 2 :] return None, s def IterableModel( - subtask_class: Type[BaseModel], + subtask_class: type[BaseModel], name: Optional[str] = None, description: Optional[str] = None, -) -> Type[BaseModel]: +) -> type[BaseModel]: """ Dynamically create a IterableModel OpenAISchema that can be used to segment multiple tasks given a base class. This creates class that can be used to create a toolkit @@ -180,7 +192,7 @@ def from_streaming_response(cls, completion) -> Generator[User]: name = f"Iterable{task_name}" list_tasks = ( - List[subtask_class], # type: ignore[valid-type] + list[subtask_class], Field( default_factory=list, repr=False, @@ -188,11 +200,14 @@ def from_streaming_response(cls, completion) -> Generator[User]: ), ) + base_models = cast(tuple[type[BaseModel], ...], (OpenAISchema, IterableBase)) new_cls = create_model( name, tasks=list_tasks, - __base__=(OpenAISchema, IterableBase), + __base__=base_models, ) + new_cls = cast(type[IterableBase], new_cls) + # set the class constructor BaseModel new_cls.task_type = subtask_class diff --git a/instructor/dsl/maybe.py b/instructor/dsl/maybe.py index 98e3ea92d..a3e92233f 100644 --- a/instructor/dsl/maybe.py +++ b/instructor/dsl/maybe.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel, Field, create_model -from typing import Generic, Optional, Type, TypeVar +from pydantic import BaseModel, Field, create_model # type: ignore - remove once Pydantic is updated +from typing import Generic, Optional, TypeVar T = TypeVar("T", bound=BaseModel) -class MaybeBase(BaseModel, Generic[T]): # type: ignore[misc] +class MaybeBase(BaseModel, Generic[T]): """ Extract a result from a model, if any, otherwise set the error and message fields. """ @@ -17,7 +17,7 @@ def __bool__(self) -> bool: return self.result is not None -def Maybe(model: Type[T]) -> Type[MaybeBase[T]]: +def Maybe(model: type[T]) -> type[MaybeBase[T]]: """ Create a Maybe model for a given Pydantic model. This allows you to return a model that includes fields for `result`, `error`, and `message` for sitatations where the data may not be present in the context. @@ -53,23 +53,22 @@ def __bool__(self): Returns: MaybeModel (Type[BaseModel]): A new Pydantic model that includes fields for `result`, `error`, and `message`. """ - - fields = { - "result": ( + return create_model( + f"Maybe{model.__name__}", + __base__=MaybeBase, + reuslts=( Optional[model], Field( default=None, description="Correctly extracted result from the model, if any, otherwise None", ), ), - "error": (bool, Field(default=False)), - "message": ( + error=(bool, Field(default=False)), + message=( Optional[str], Field( default=None, description="Error message if no result was found, should be short and concise", ), ), - } - - return create_model(f"Maybe{model.__name__}", __base__=MaybeBase, **fields) + ) diff --git a/instructor/dsl/parallel.py b/instructor/dsl/parallel.py index 831c1c097..9fa17b1a3 100644 --- a/instructor/dsl/parallel.py +++ b/instructor/dsl/parallel.py @@ -1,30 +1,30 @@ from typing import ( Any, - Dict, - Generator, - List, Optional, - Tuple, - Type, TypeVar, Union, get_args, get_origin, ) -from types import UnionType # type: ignore[attr-defined] - -from instructor.function_calls import OpenAISchema, Mode, openai_schema +from collections.abc import Generator +from pydantic import BaseModel +from instructor.function_calls import OpenAISchema, openai_schema from collections.abc import Iterable +from instructor.mode import Mode + T = TypeVar("T", bound=OpenAISchema) class ParallelBase: - def __init__(self, *models: Type[OpenAISchema]): + def __init__(self, *models: type[OpenAISchema]): # Note that for everything else we've created a class, but for parallel base it is an instance assert len(models) > 0, "At least one model is required" self.models = models - self.registry = {model.__name__: model for model in models} + self.registry = { + model.__name__ if hasattr(model, "__name__") else str(model): model + for model in models + } def from_response( self, @@ -32,7 +32,7 @@ def from_response( mode: Mode, validation_context: Optional[Any] = None, strict: Optional[bool] = None, - ) -> Generator[T, None, None]: + ) -> Generator[BaseModel, None, None]: #! We expect this from the OpenAISchema class, We should address #! this with a protocol or an abstract class... @jxnlco assert mode == Mode.PARALLEL_TOOLS, "Mode must be PARALLEL_TOOLS" @@ -44,7 +44,7 @@ def from_response( ) -def get_types_array(typehint: Type[Iterable[Union[T]]]) -> Tuple[Type[T], ...]: +def get_types_array(typehint: type[Iterable[T]]) -> tuple[type[T], ...]: should_be_iterable = get_origin(typehint) if should_be_iterable is not Iterable: raise TypeError(f"Model should be with Iterable instead if {typehint}") @@ -54,7 +54,7 @@ def get_types_array(typehint: Type[Iterable[Union[T]]]) -> Tuple[Type[T], ...]: the_types = get_args(get_args(typehint)[0]) return the_types - if get_origin(get_args(typehint)[0]) is UnionType: + if get_origin(get_args(typehint)[0]) is Union: # works for Iterable[Union[int, str]] the_types = get_args(get_args(typehint)[0]) return the_types @@ -63,7 +63,7 @@ def get_types_array(typehint: Type[Iterable[Union[T]]]) -> Tuple[Type[T], ...]: return get_args(typehint) -def handle_parallel_model(typehint: Type[Iterable[Union[T]]]) -> List[Dict[str, Any]]: +def handle_parallel_model(typehint: type[Iterable[T]]) -> list[dict[str, Any]]: the_types = get_types_array(typehint) return [ {"type": "function", "function": openai_schema(model).openai_schema} @@ -71,6 +71,6 @@ def handle_parallel_model(typehint: Type[Iterable[Union[T]]]) -> List[Dict[str, ] -def ParallelModel(typehint: Type[Iterable[Union[T]]]) -> ParallelBase: +def ParallelModel(typehint: type[Iterable[T]]) -> ParallelBase: the_types = get_types_array(typehint) return ParallelBase(*[model for model in the_types]) diff --git a/instructor/dsl/partial.py b/instructor/dsl/partial.py index cb4cc028a..759dbff47 100644 --- a/instructor/dsl/partial.py +++ b/instructor/dsl/partial.py @@ -6,88 +6,143 @@ # serves as an acknowledgment of the original author's contribution to this project. # -------------------------------------------------------------------------------- -from pydantic import BaseModel, create_model +from __future__ import annotations + +import pydantic_core +from pydantic import BaseModel, create_model # type: ignore - remove once Pydantic is updated from pydantic.fields import FieldInfo from typing import ( Any, - AsyncGenerator, - Generator, Generic, get_args, get_origin, - Iterable, NoReturn, Optional, TypeVar, ) +from collections.abc import AsyncGenerator, Generator, Iterable from copy import deepcopy +from functools import cache + +from instructor.mode import Mode +from instructor.utils import extract_json_from_stream, extract_json_from_stream_async + +T_Model = TypeVar("T_Model", bound=BaseModel) + + +class MakeFieldsOptional: + pass -from instructor.function_calls import Mode -from instructor.dsl.partialjson import JSONParser -parser = JSONParser() +def _make_field_optional( + field: FieldInfo, +) -> tuple[Any, FieldInfo]: + tmp_field = deepcopy(field) -Model = TypeVar("Model", bound=BaseModel) + annotation = field.annotation + # Handle generics (like List, Dict, etc.) + if get_origin(annotation) is not None: + # Get the generic base (like List, Dict) and its arguments (like User in List[User]) + generic_base = get_origin(annotation) + generic_args = get_args(annotation) + + # Recursively apply Partial to each of the generic arguments + modified_args = tuple( + ( + Partial[arg, MakeFieldsOptional] # type: ignore[valid-type] + if isinstance(arg, type) and issubclass(arg, BaseModel) + else arg + ) + for arg in generic_args + ) + + # Reconstruct the generic type with modified arguments + tmp_field.annotation = ( + Optional[generic_base[modified_args]] if generic_base else None + ) + tmp_field.default = None + # If the field is a BaseModel, then recursively convert it's + # attributes to optionals. + elif isinstance(annotation, type) and issubclass(annotation, BaseModel): + tmp_field.annotation = Optional[Partial[annotation, MakeFieldsOptional]] # type: ignore[assignment, valid-type] + tmp_field.default = {} + else: + tmp_field.annotation = Optional[field.annotation] # type: ignore[assignment] + tmp_field.default = None + + return tmp_field.annotation, tmp_field # type: ignore + + +class PartialBase(Generic[T_Model]): + @classmethod + @cache + def get_partial_model(cls) -> type[T_Model]: + """Return a partial model we can use to validate partial results.""" + assert issubclass( + cls, BaseModel + ), f"{cls.__name__} must be a subclass of BaseModel" + + return create_model( + __model_name=( + cls.__name__ + if cls.__name__.startswith("Partial") + else f"Partial{cls.__name__}" + ), + __base__=cls, + __module__=cls.__module__, + **{ + field_name: _make_field_optional(field_info) + for field_name, field_info in cls.model_fields.items() + }, + ) # type: ignore[all] -class PartialBase: @classmethod def from_streaming_response( cls, completion: Iterable[Any], mode: Mode, **kwargs: Any - ) -> Generator[Model, None, None]: + ) -> Generator[T_Model, None, None]: json_chunks = cls.extract_json(completion, mode) + + if mode == Mode.MD_JSON: + json_chunks = extract_json_from_stream(json_chunks) + yield from cls.model_from_chunks(json_chunks, **kwargs) @classmethod async def from_streaming_response_async( cls, completion: AsyncGenerator[Any, None], mode: Mode, **kwargs: Any - ) -> AsyncGenerator[Model, None]: + ) -> AsyncGenerator[T_Model, None]: json_chunks = cls.extract_json_async(completion, mode) + + if mode == Mode.MD_JSON: + json_chunks = extract_json_from_stream_async(json_chunks) + return cls.model_from_chunks_async(json_chunks, **kwargs) @classmethod def model_from_chunks( cls, json_chunks: Iterable[Any], **kwargs: Any - ) -> Generator[Model, None, None]: - prev_obj = None + ) -> Generator[T_Model, None, None]: potential_object = "" + partial_model = cls.get_partial_model() for chunk in json_chunks: potential_object += chunk - # Avoid parsing incomplete json when its just whitespace otherwise parser throws an exception - task_json = ( - parser.parse(potential_object) if potential_object.strip() else None - ) - if task_json: - obj = cls.model_validate(task_json, strict=None, **kwargs) # type: ignore[attr-defined] - if obj != prev_obj: - obj.__dict__[ - "chunk" - ] = chunk # Provide the raw chunk for debugging and benchmarking - prev_obj = obj - yield obj + obj = pydantic_core.from_json(potential_object or "{}", allow_partial=True) + obj = partial_model.model_validate(obj, strict=None, **kwargs) + yield obj @classmethod async def model_from_chunks_async( cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any - ) -> AsyncGenerator[Model, None]: + ) -> AsyncGenerator[T_Model, None]: potential_object = "" - prev_obj = None + partial_model = cls.get_partial_model() async for chunk in json_chunks: potential_object += chunk - - # Avoid parsing incomplete json when its just whitespace otherwise parser throws an exception - task_json = ( - parser.parse(potential_object) if potential_object.strip() else None - ) - if task_json: - obj = cls.model_validate(task_json, strict=None, **kwargs) # type: ignore[attr-defined] - if obj != prev_obj: - obj.__dict__[ - "chunk" - ] = chunk # Provide the raw chunk for debugging and benchmarking - prev_obj = obj - yield obj + obj = pydantic_core.from_json(potential_object or "{}", allow_partial=True) + obj = partial_model.model_validate(obj, strict=None, **kwargs) + yield obj @staticmethod def extract_json( @@ -136,12 +191,11 @@ async def extract_json_async( pass -class Partial(Generic[Model]): - """Generate a new class with all attributes optionals. +class Partial(Generic[T_Model]): + """Generate a new class which has PartialBase as a base class. Notes: - This will wrap a class inheriting form BaseModel and will recursively - convert all its attributes and its children's attributes to optionals. + This will enable partial validation of the model while streaming. Example: Partial[SomeModel] @@ -151,7 +205,7 @@ def __new__( cls, *args: object, # noqa :ARG003 **kwargs: object, # noqa :ARG003 - ) -> "Partial[Model]": + ) -> Partial[T_Model]: """Cannot instantiate. Raises: @@ -169,17 +223,27 @@ def __init_subclass__( Raises: TypeError: Subclassing not allowed. """ - raise TypeError("Cannot subclass {}.Partial".format(cls.__module__)) + raise TypeError(f"Cannot subclass {cls.__module__}.Partial") - def __class_getitem__( # type: ignore[override] + def __class_getitem__( cls, - wrapped_class: type[Model], - ) -> type[Model]: - """Convert model to a partial model with all fields being optionals.""" + wrapped_class: type[T_Model] | tuple[type[T_Model], type[MakeFieldsOptional]], + ) -> type[T_Model]: + """Convert model to one that inherits from PartialBase. - def _make_field_optional( - field: FieldInfo, - ) -> tuple[object, FieldInfo]: + We don't make the fields optional at this point, we just wrap them with `Partial` so the names of the nested models will be + `Partial{ModelName}`. We want the output of `model_json_schema()` to + reflect the name change, but everything else should be the same as the + original model. During validation, we'll generate a true partial model + to support partially defined fields. + + """ + + make_fields_optional = None + if isinstance(wrapped_class, tuple): + wrapped_class, make_fields_optional = wrapped_class + + def _wrap_models(field: FieldInfo) -> tuple[object, FieldInfo]: tmp_field = deepcopy(field) annotation = field.annotation @@ -192,31 +256,38 @@ def _make_field_optional( # Recursively apply Partial to each of the generic arguments modified_args = tuple( - Partial[arg] # type: ignore[valid-type] - if isinstance(arg, type) and issubclass(arg, BaseModel) - else arg + ( + Partial[arg] + if isinstance(arg, type) and issubclass(arg, BaseModel) + else arg + ) for arg in generic_args ) # Reconstruct the generic type with modified arguments - tmp_field.annotation = Optional[generic_base[modified_args]] - tmp_field.default = None + tmp_field.annotation = ( + generic_base[modified_args] if generic_base else None + ) # If the field is a BaseModel, then recursively convert it's # attributes to optionals. elif isinstance(annotation, type) and issubclass(annotation, BaseModel): - tmp_field.annotation = Optional[Partial[annotation]] # type: ignore[assignment, valid-type] - tmp_field.default = {} - else: - tmp_field.annotation = Optional[field.annotation] # type: ignore[assignment] - tmp_field.default = None + tmp_field.annotation = Partial[annotation] return tmp_field.annotation, tmp_field - return create_model( # type: ignore[no-any-return, call-overload] - f"Partial{wrapped_class.__name__}", + return create_model( + __model_name=( + wrapped_class.__name__ + if wrapped_class.__name__.startswith("Partial") + else f"Partial{wrapped_class.__name__}" + ), __base__=(wrapped_class, PartialBase), __module__=wrapped_class.__module__, **{ - field_name: _make_field_optional(field_info) + field_name: ( + _make_field_optional(field_info) + if make_fields_optional is not None + else _wrap_models(field_info) + ) for field_name, field_info in wrapped_class.model_fields.items() }, - ) + ) # type: ignore diff --git a/instructor/dsl/partialjson.py b/instructor/dsl/partialjson.py deleted file mode 100644 index 3e215e330..000000000 --- a/instructor/dsl/partialjson.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -partialjson - Parse Partial and incomplete JSON in python -Copyright (c) 2023 Nima Akbarzadeh - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" -from typing import Any, Dict, Optional, Tuple -import json - - -class JSONParser: - def __init__(self) -> None: - self.parsers = { - " ": self.parse_space, - "\r": self.parse_space, - "\n": self.parse_space, - "\t": self.parse_space, - "[": self.parse_array, - "{": self.parse_object, - '"': self.parse_string, - "t": self.parse_true, - "f": self.parse_false, - "n": self.parse_null, - } - # Adding parsers for numbers - for c in "0123456789.-": - self.parsers[c] = self.parse_number - - self.last_parse_reminding: Optional[str] = None - self.on_extra_token = self.default_on_extra_token - - def default_on_extra_token(self, text: str, data: Any, reminding: str) -> None: - pass - - def parse(self, s: str) -> Dict[str, Any]: - if len(s) >= 1: - try: - return json.loads(s) - except json.JSONDecodeError as e: - data, reminding = self.parse_any(s, e) - self.last_parse_reminding = reminding - if self.on_extra_token is not None and reminding: - self.on_extra_token(s, data, reminding) # type: ignore[no-untyped-call] - return json.loads(json.dumps(data)) - else: - return json.loads("{}") - - def parse_any(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - if not s: - raise e - parser = self.parsers.get(s[0]) - if not parser: - raise e - return parser(s, e) - - def parse_space(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - return self.parse_any(s.strip(), e) - - def parse_array(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - s = s[1:] # skip starting '[' - acc = [] - s = s.strip() - while s: - if s[0] == "]": - s = s[1:] # skip ending ']' - break - res, s = self.parse_any(s, e) - acc.append(res) - s = s.strip() - if s.startswith(","): - s = s[1:] - s = s.strip() - return acc, s - - def parse_object(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - s = s[1:] # skip starting '{' - acc: Dict[str, Any] = {} - s = s.strip() - while s: - if s[0] == "}": - s = s[1:] # skip ending '}' - break - key, s = self.parse_any(s, e) - s = s.strip() - - # Handle case where object ends after a key - if not s or s[0] == "}": - acc[key] = None - break - - # Expecting a colon after the key - if s[0] != ":": - raise e # or handle this scenario as per your requirement - - s = s[1:] # skip ':' - s = s.strip() - - # Handle case where value is missing or incomplete - if not s or s[0] in ",}": - acc[key] = None - if s.startswith(","): - s = s[1:] - break - - value, s = self.parse_any(s, e) - acc[key] = value - s = s.strip() - if s.startswith(","): - s = s[1:] - s = s.strip() - return acc, s - - def parse_string(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: # noqa: ARG002 - end = s.find('"', 1) - while end != -1 and s[end - 1] == "\\": # Handle escaped quotes - end = s.find('"', end + 1) - if end == -1: - # Return the incomplete string without the opening quote - return s[1:], "" - str_val = s[: end + 1] - s = s[end + 1 :] - return json.loads(str_val), s - - def parse_number(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - i = 0 - while i < len(s) and s[i] in "0123456789.-": - i += 1 - num_str = s[:i] - s = s[i:] - if not num_str or num_str.endswith(".") or num_str.endswith("-"): - return num_str, "" # Return the incomplete number as is - try: - num = ( - float(num_str) - if "." in num_str or "e" in num_str or "E" in num_str - else int(num_str) - ) - except ValueError as e: - raise e - return num, s - - def parse_true(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - if s.startswith("true"): - return True, s[4:] - raise e - - def parse_false(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - if s.startswith("false"): - return False, s[5:] - raise e - - def parse_null(self, s: str, e: json.JSONDecodeError) -> Tuple[Any, str]: - if s.startswith("null"): - return None, s[4:] - raise e diff --git a/instructor/dsl/simple_type.py b/instructor/dsl/simple_type.py index 42f14820c..110ff5b2c 100644 --- a/instructor/dsl/simple_type.py +++ b/instructor/dsl/simple_type.py @@ -1,6 +1,7 @@ +from __future__ import annotations from inspect import isclass import typing -from pydantic import BaseModel, create_model +from pydantic import BaseModel, create_model # type: ignore - remove once Pydantic is updated from enum import Enum @@ -20,7 +21,7 @@ class ModelAdapter(typing.Generic[T]): Accepts a response model and returns a BaseModel with the response model as the content. """ - def __class_getitem__(cls, response_model) -> typing.Type[BaseModel]: + def __class_getitem__(cls, response_model: type[BaseModel]) -> type[BaseModel]: assert is_simple_type(response_model), "Only simple types are supported" tmp = create_model( "Response", @@ -31,7 +32,9 @@ def __class_getitem__(cls, response_model) -> typing.Type[BaseModel]: return tmp -def is_simple_type(response_model) -> bool: +def is_simple_type( + response_model: type[BaseModel] | str | int | float | bool, +) -> bool: # ! we're getting mixes between classes and instances due to how we handle some # ! response model types, we should fix this in later PRs if isclass(response_model) and issubclass(response_model, BaseModel): diff --git a/instructor/dsl/validators.py b/instructor/dsl/validators.py index 90365d8f9..d6fa99c44 100644 --- a/instructor/dsl/validators.py +++ b/instructor/dsl/validators.py @@ -4,7 +4,7 @@ from pydantic import Field from instructor.function_calls import OpenAISchema -from instructor.patch import patch +from instructor.client import Instructor class Validator(OpenAISchema): @@ -29,10 +29,10 @@ class Validator(OpenAISchema): def llm_validator( statement: str, + client: Instructor, allow_override: bool = False, model: str = "gpt-3.5-turbo", temperature: float = 0, - openai_client: OpenAI = None, ) -> Callable[[str], str]: """ Create a validator that uses the LLM to validate an attribute @@ -56,7 +56,7 @@ class User(BaseModel): ``` 1 validation error for User name - The name is valid but not all lowercase (type=value_error.llm_validator) + The name is valid but not all lowercase (type=value_error.llm_validator) ``` Note that there, the error message is written by the LLM, and the error type is `value_error.llm_validator`. @@ -68,10 +68,8 @@ class User(BaseModel): openai_client (OpenAI): The OpenAI client to use (default: None) """ - openai_client = openai_client if openai_client else patch(OpenAI()) - def llm(v: str) -> str: - resp = openai_client.chat.completions.create( + resp = client.chat.completions.create( response_model=Validator, messages=[ { @@ -99,7 +97,7 @@ def llm(v: str) -> str: return llm -def openai_moderation(client: Optional[OpenAI] = None) -> Callable[[str], str]: +def openai_moderation(client: OpenAI) -> Callable[[str], str]: """ Validates a message using OpenAI moderation model. @@ -126,8 +124,6 @@ class Response(BaseModel): client (OpenAI): The OpenAI client to use, must be sync (default: None) """ - client = client or OpenAI() - def validate_message_with_openai_mod(v: str) -> str: response = client.moderations.create(input=v) out = response.results[0] diff --git a/instructor/function_calls.py b/instructor/function_calls.py index ce5467f67..28de8efe1 100644 --- a/instructor/function_calls.py +++ b/instructor/function_calls.py @@ -1,44 +1,33 @@ -from typing import Any, Dict, Optional, Type, TypeVar -from docstring_parser import parse +import json +import logging from functools import wraps -from pydantic import BaseModel, create_model -from instructor.exceptions import IncompleteOutputException -import enum -import warnings - -T = TypeVar("T") +from typing import Annotated, Any, Optional, TypeVar, cast +from docstring_parser import parse +from openai.types.chat import ChatCompletion +from pydantic import ( # type: ignore - remove once Pydantic is updated + BaseModel, + ConfigDict, + Field, + TypeAdapter, + create_model, +) -class Mode(enum.Enum): - """The mode to use for patching the client""" - - FUNCTIONS: str = "function_call" - PARALLEL_TOOLS: str = "parallel_tool_call" - TOOLS: str = "tool_call" - MISTRAL_TOOLS: str = "mistral_tools" - JSON: str = "json_mode" - MD_JSON: str = "markdown_json_mode" - JSON_SCHEMA: str = "json_schema_mode" +from instructor.exceptions import IncompleteOutputException +from instructor.mode import Mode +from instructor.utils import classproperty, extract_json_from_codeblock - def __new__(cls, value: str) -> "Mode": - member = object.__new__(cls) - member._value_ = value +T = TypeVar("T") - # Deprecation warning for FUNCTIONS - if value == "function_call": - warnings.warn( - "FUNCTIONS is deprecated and will be removed in future versions", - DeprecationWarning, - stacklevel=2, - ) +logger = logging.getLogger("instructor") - return member +class OpenAISchema(BaseModel): + # Ignore classproperty, since Pydantic doesn't understand it like it would a normal property. + model_config = ConfigDict(ignored_types=(classproperty,)) -class OpenAISchema(BaseModel): # type: ignore[misc] - @classmethod # type: ignore[misc] - @property - def openai_schema(cls) -> Dict[str, Any]: + @classproperty + def openai_schema(cls) -> dict[str, Any]: """ Return the schema in the format of OpenAI's schema as jsonschema @@ -79,14 +68,22 @@ def openai_schema(cls) -> Dict[str, Any]: "parameters": parameters, } + @classproperty + def anthropic_schema(cls) -> dict[str, Any]: + return { + "name": cls.openai_schema["name"], + "description": cls.openai_schema["description"], + "input_schema": cls.model_json_schema(), + } + @classmethod def from_response( cls, - completion: T, - validation_context: Optional[Dict[str, Any]] = None, + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, strict: Optional[bool] = None, mode: Mode = Mode.TOOLS, - ) -> Dict[str, Any]: + ) -> BaseModel: """Execute the function from the response of an openai chat completion Parameters: @@ -99,52 +96,145 @@ def from_response( Returns: cls (OpenAISchema): An instance of the class """ - assert hasattr(completion, "choices") + if mode == Mode.ANTHROPIC_TOOLS: + return cls.parse_anthropic_tools(completion, validation_context, strict) + + if mode == Mode.ANTHROPIC_JSON: + return cls.parse_anthropic_json(completion, validation_context, strict) + + if mode == Mode.COHERE_TOOLS: + return cls.parse_cohere_tools(completion, validation_context, strict) if completion.choices[0].finish_reason == "length": raise IncompleteOutputException() - message = completion.choices[0].message - if mode == Mode.FUNCTIONS: - assert ( - message.function_call.name == cls.openai_schema["name"] # type: ignore[index] - ), "Function name does not match" - return cls.model_validate_json( - message.function_call.arguments, - context=validation_context, - strict=strict, - ) - elif mode in {Mode.TOOLS, Mode.MISTRAL_TOOLS}: - assert ( - len(message.tool_calls) == 1 - ), "Instructor does not support multiple tool calls, use List[Model] instead." - tool_call = message.tool_calls[0] - assert ( - tool_call.function.name == cls.openai_schema["name"] # type: ignore[index] - ), "Tool name does not match" - return cls.model_validate_json( - tool_call.function.arguments, - context=validation_context, - strict=strict, - ) - elif mode in {Mode.JSON, Mode.JSON_SCHEMA, Mode.MD_JSON}: + return cls.parse_functions(completion, validation_context, strict) + + if mode in {Mode.TOOLS, Mode.MISTRAL_TOOLS}: + return cls.parse_tools(completion, validation_context, strict) + + if mode in {Mode.JSON, Mode.JSON_SCHEMA, Mode.MD_JSON}: + return cls.parse_json(completion, validation_context, strict) + + raise ValueError(f"Invalid patch mode: {mode}") + + @classmethod + def parse_anthropic_tools( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + tool_calls = [c.input for c in completion.content if c.type == "tool_use"] # type: ignore - TODO update with anthropic specific types + + tool_calls_validator = TypeAdapter( + Annotated[list[Any], Field(min_length=1, max_length=1)] + ) + tool_call = tool_calls_validator.validate_python(tool_calls)[0] + + return cls.model_validate(tool_call, context=validation_context, strict=strict) + + @classmethod + def parse_anthropic_json( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + from anthropic.types import Message + + assert isinstance(completion, Message) + + text = completion.content[0].text + extra_text = extract_json_from_codeblock(text) + + if strict: return cls.model_validate_json( - message.content, - context=validation_context, - strict=strict, + extra_text, context=validation_context, strict=True ) else: - raise ValueError(f"Invalid patch mode: {mode}") + # Allow control characters. + parsed = json.loads(extra_text, strict=False) + # Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/ + return cls.model_validate(parsed, context=validation_context, strict=False) + + @classmethod + def parse_cohere_tools( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + text = cast(str, completion.text) # type: ignore - TODO update with cohere specific types + extra_text = extract_json_from_codeblock(text) + return cls.model_validate_json( + extra_text, context=validation_context, strict=strict + ) + + @classmethod + def parse_functions( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + message = completion.choices[0].message + assert ( + message.function_call.name == cls.openai_schema["name"] # type: ignore[index] + ), "Function name does not match" + return cls.model_validate_json( + message.function_call.arguments, # type: ignore[attr-defined] + context=validation_context, + strict=strict, + ) + + @classmethod + def parse_tools( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + message = completion.choices[0].message + assert ( + len(message.tool_calls or []) == 1 + ), "Instructor does not support multiple tool calls, use List[Model] instead." + tool_call = message.tool_calls[0] # type: ignore + assert ( + tool_call.function.name == cls.openai_schema["name"] # type: ignore[index] + ), "Tool name does not match" + return cls.model_validate_json( + tool_call.function.arguments, # type: ignore + context=validation_context, + strict=strict, + ) + + @classmethod + def parse_json( + cls: type[BaseModel], + completion: ChatCompletion, + validation_context: Optional[dict[str, Any]] = None, + strict: Optional[bool] = None, + ) -> BaseModel: + message = completion.choices[0].message.content or "" + message = extract_json_from_codeblock(message) + + return cls.model_validate_json( + message, + context=validation_context, + strict=strict, + ) -def openai_schema(cls: Type[BaseModel]) -> OpenAISchema: +def openai_schema(cls: type[BaseModel]) -> OpenAISchema: if not issubclass(cls, BaseModel): raise TypeError("Class must be a subclass of pydantic.BaseModel") - return wraps(cls, updated=())( + shema = wraps(cls, updated=())( create_model( - cls.__name__, + cls.__name__ if hasattr(cls, "__name__") else str(cls), __base__=(cls, OpenAISchema), ) ) + return cast(OpenAISchema, shema) diff --git a/instructor/mode.py b/instructor/mode.py new file mode 100644 index 000000000..f40d02dc2 --- /dev/null +++ b/instructor/mode.py @@ -0,0 +1,31 @@ +import enum +import warnings + + +class Mode(enum.Enum): + """The mode to use for patching the client""" + + FUNCTIONS = "function_call" + PARALLEL_TOOLS = "parallel_tool_call" + TOOLS = "tool_call" + MISTRAL_TOOLS = "mistral_tools" + JSON = "json_mode" + MD_JSON = "markdown_json_mode" + JSON_SCHEMA = "json_schema_mode" + ANTHROPIC_TOOLS = "anthropic_tools" + ANTHROPIC_JSON = "anthropic_json" + COHERE_TOOLS = "cohere_tools" + + def __new__(cls, value: str) -> "Mode": + member = object.__new__(cls) + member._value_ = value + + # Deprecation warning for FUNCTIONS + if value == "function_call": + warnings.warn( + "FUNCTIONS is deprecated and will be removed in future versions", + DeprecationWarning, + stacklevel=2, + ) + + return member diff --git a/instructor/patch.py b/instructor/patch.py index 63e85dfb5..9a1b2a0ae 100644 --- a/instructor/patch.py +++ b/instructor/patch.py @@ -1,533 +1,80 @@ -import inspect -import json -import logging -from collections.abc import Iterable +# type: ignore[all] from functools import wraps -from tenacity import Retrying, AsyncRetrying, stop_after_attempt, RetryError -from json import JSONDecodeError from typing import ( Callable, - Optional, - ParamSpec, Protocol, - Type, TypeVar, Union, - get_args, - get_origin, overload, ) +from collections.abc import Awaitable +from typing_extensions import ParamSpec from openai import AsyncOpenAI, OpenAI -from openai.types.chat import ( - ChatCompletion, - ChatCompletionMessage, - ChatCompletionMessageParam, -) -from openai.types.completion_usage import CompletionUsage -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel -from instructor.dsl.iterable import IterableModel, IterableBase -from instructor.dsl.parallel import ParallelBase, ParallelModel, handle_parallel_model -from instructor.dsl.partial import PartialBase -from instructor.dsl.simple_type import ModelAdapter, AdapterBase, is_simple_type +from instructor.process_response import handle_response_model +from instructor.retry import retry_async, retry_sync +from instructor.utils import is_async -from .function_calls import Mode, OpenAISchema, openai_schema +from instructor.mode import Mode +import logging logger = logging.getLogger("instructor") -T = TypeVar("T") - T_Model = TypeVar("T_Model", bound=BaseModel) T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") -T = TypeVar("T") - - -def dump_message(message: ChatCompletionMessage) -> ChatCompletionMessageParam: - """Dumps a message to a dict, to be returned to the OpenAI API. - Workaround for an issue with the OpenAI API, where the `tool_calls` field isn't allowed to be present in requests - if it isn't used. - """ - ret: ChatCompletionMessageParam = { - "role": message.role, - "content": message.content or "", - } - if hasattr(message, "tool_calls") and message.tool_calls is not None: - ret["tool_calls"] = message.model_dump()["tool_calls"] - if hasattr(message, "function_call") and message.function_call is not None: - ret["content"] += json.dumps(message.model_dump()["function_call"]) - return ret - - -def handle_response_model( - response_model: T, mode: Mode = Mode.TOOLS, **kwargs -) -> Union[Type[OpenAISchema], dict]: - """Prepare the response model type hint, and returns the response_model - along with the new modified kwargs needed to be able to use the response_model - parameter with the patch function. - - - Args: - response_model (T): The response model to use for parsing the response - mode (Mode, optional): The openai completion mode. Defaults to Mode.TOOLS. - - Raises: - NotImplementedError: When using stream=True with a non-iterable response_model - ValueError: When using an invalid patch mode - - Returns: - Union[Type[OpenAISchema], dict]: The response model to use for parsing the response - """ - new_kwargs = kwargs.copy() - if response_model is not None: - # Handles the case where the response_model is a simple type - # Literal, Annotated, Union, str, int, float, bool, Enum - # We wrap the response_model in a ModelAdapter that sets 'content' as the response - if is_simple_type(response_model): - response_model = ModelAdapter[response_model] - - # This a special case for parallel tools - if mode == Mode.PARALLEL_TOOLS: - assert ( - new_kwargs.get("stream", False) is False - ), "stream=True is not supported when using PARALLEL_TOOLS mode" - new_kwargs["tools"] = handle_parallel_model(response_model) - new_kwargs["tool_choice"] = "auto" - - # This is a special case for parallel models - response_model = ParallelModel(typehint=response_model) - return response_model, new_kwargs - - # This is for all other single model cases - if get_origin(response_model) is Iterable: - iterable_element_class = get_args(response_model)[0] - response_model = IterableModel(iterable_element_class) - if not issubclass(response_model, OpenAISchema): - response_model = openai_schema(response_model) # type: ignore - - if new_kwargs.get("stream", False) and not issubclass( - response_model, (IterableBase, PartialBase) - ): - raise NotImplementedError( - "stream=True is not supported when using response_model parameter for non-iterables" - ) - - if mode == Mode.FUNCTIONS: - new_kwargs["functions"] = [response_model.openai_schema] # type: ignore - new_kwargs["function_call"] = {"name": response_model.openai_schema["name"]} # type: ignore - elif mode in {Mode.TOOLS, Mode.MISTRAL_TOOLS}: - new_kwargs["tools"] = [ - { - "type": "function", - "function": response_model.openai_schema, - } - ] - if mode == Mode.MISTRAL_TOOLS: - new_kwargs["tool_choice"] = "any" - else: - new_kwargs["tool_choice"] = { - "type": "function", - "function": {"name": response_model.openai_schema["name"]}, - } - elif mode in {Mode.JSON, Mode.MD_JSON, Mode.JSON_SCHEMA}: - # If its a JSON Mode we need to massage the prompt a bit - # in order to get the response we want in a json format - message = f""" - As a genius expert, your task is to understand the content and provide - the parsed objects in json that match the following json_schema:\n - {response_model.model_json_schema()['properties']} - """ - # Check for nested models - if "$defs" in response_model.model_json_schema(): - message += f"\nHere are some more definitions to adhere too:\n{response_model.model_json_schema()['$defs']}" - - if mode == Mode.JSON: - new_kwargs["response_format"] = {"type": "json_object"} - - elif mode == Mode.JSON_SCHEMA: - new_kwargs["response_format"] = { - "type": "json_object", - "schema": response_model.model_json_schema(), - } - - elif mode == Mode.MD_JSON: - new_kwargs["messages"].append( - { - "role": "assistant", - "content": "Here is the perfectly correctly formatted JSON\n```json", - }, - ) - new_kwargs["stop"] = "```" - # check that the first message is a system message - # if it is not, add a system message to the beginning - if new_kwargs["messages"][0]["role"] != "system": - new_kwargs["messages"].insert( - 0, - { - "role": "system", - "content": message, - }, - ) - # if it is, system append the schema to the end - else: - new_kwargs["messages"][0]["content"] += f"\n\n{message}" - else: - raise ValueError(f"Invalid patch mode: {mode}") - return response_model, new_kwargs - - -def process_response( - response: T, - *, - response_model: Type[T_Model], - stream: bool, - validation_context: dict = None, - strict=None, - mode: Mode = Mode.TOOLS, -) -> Union[T_Model, T]: - """Processes a OpenAI response with the response model, if available. - - Args: - response (T): The response from OpenAI's API - response_model (Type[T_Model]): The response model to use for parsing the response - stream (bool): Whether the response is a stream - validation_context (dict, optional): The validation context to use for validating the response. Defaults to None. - strict (_type_, optional): Whether to use strict json parsing. Defaults to None. - mode (Mode, optional): The openai completion mode. Defaults to Mode.FUNCTIONS. - - Returns: - Union[T_Model, T]: The parsed response, if a response model is available, otherwise the response as is from the SDK - """ - if response_model is None: - return response - - if ( - inspect.isclass(response_model) - and issubclass(response_model, (IterableBase, PartialBase)) - and stream - ): - model = response_model.from_streaming_response( - response, - mode=mode, - ) - return model - - model = response_model.from_response( - response, - validation_context=validation_context, - strict=strict, - mode=mode, - ) - - # ? This really hints at the fact that we need a better way of - # ? attaching usage data and the raw response to the model we return. - if isinstance(model, IterableBase): - logger.debug(f"Returning takes from IterableBase") - return [task for task in model.tasks] - - if isinstance(response_model, ParallelBase): - logger.debug(f"Returning model from ParallelBase") - return model - - if isinstance(model, AdapterBase): - logger.debug(f"Returning model from AdapterBase") - return model.content - - model._raw_response = response - return model - - -async def process_response_async( - response: ChatCompletion, - *, - response_model: Type[T_Model], - stream: bool = False, - validation_context: dict = None, - strict: Optional[bool] = None, - mode: Mode = Mode.TOOLS, -) -> T: - """Processes a OpenAI response with the response model, if available. - It can use `validation_context` and `strict` to validate the response - via the pydantic model - - Args: - response (ChatCompletion): The response from OpenAI's API - response_model (BaseModel): The response model to use for parsing the response - stream (bool): Whether the response is a stream - validation_context (dict, optional): The validation context to use for validating the response. Defaults to None. - strict (bool, optional): Whether to use strict json parsing. Defaults to None. - """ - if response_model is None: - return response - - if ( - inspect.isclass(response_model) - and issubclass(response_model, (IterableBase, PartialBase)) - and stream - ): - model = await response_model.from_streaming_response_async( - response, - mode=mode, - ) - return model - - model = response_model.from_response( - response, - validation_context=validation_context, - strict=strict, - mode=mode, - ) - - # ? This really hints at the fact that we need a better way of - # ? attaching usage data and the raw response to the model we return. - if isinstance(model, IterableBase): - logger.debug(f"Returning takes from IterableBase") - return [task for task in model.tasks] - - if isinstance(response_model, ParallelBase): - logger.debug(f"Returning model from ParallelBase") - return model - - if isinstance(model, AdapterBase): - logger.debug(f"Returning model from AdapterBase") - return model.content - - model._raw_response = response - return model - - -async def retry_async( - func: Callable[T_ParamSpec, T_Retval], - response_model: Type[T], - validation_context, - args, - kwargs, - max_retries: int | AsyncRetrying = 1, - strict: Optional[bool] = None, - mode: Mode = Mode.TOOLS, -) -> T: - total_usage = CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0) - - # If max_retries is int, then create a AsyncRetrying object - if isinstance(max_retries, int): - logger.debug(f"max_retries: {max_retries}") - max_retries = AsyncRetrying( - stop=stop_after_attempt(max_retries), - reraise=True, - ) - if not isinstance(max_retries, (AsyncRetrying, Retrying)): - raise ValueError( - "max_retries must be an `int` or a `tenacity.AsyncRetrying` object" - ) - - try: - async for attempt in max_retries: - logger.debug(f"Retrying, attempt: {attempt}") - with attempt: - try: - response: ChatCompletion = await func(*args, **kwargs) - stream = kwargs.get("stream", False) - if ( - isinstance(response, ChatCompletion) - and response.usage is not None - ): - total_usage.completion_tokens += ( - response.usage.completion_tokens or 0 - ) - total_usage.prompt_tokens += response.usage.prompt_tokens or 0 - total_usage.total_tokens += response.usage.total_tokens or 0 - response.usage = total_usage # Replace each response usage with the total usage - return await process_response_async( - response, - response_model=response_model, - stream=stream, - validation_context=validation_context, - strict=strict, - mode=mode, - ) - except (ValidationError, JSONDecodeError) as e: - logger.debug(f"Error response: {response}") - kwargs["messages"].append(dump_message(response.choices[0].message)) # type: ignore - if mode == Mode.TOOLS: - kwargs["messages"].append( - { - "role": "tool", - "tool_call_id": response.choices[0] - .message.tool_calls[0] - .id, - "name": response.choices[0] - .message.tool_calls[0] - .function.name, - "content": "Exceptions found\n{e}\nRecall the function correctly.", - } - ) - - kwargs["messages"].append( - { - "role": "user", - "content": f"Recall the function correctly, fix the errors, exceptions found\n{e}", - } - ) - if mode == Mode.MD_JSON: - kwargs["messages"].append( - { - "role": "assistant", - "content": "```json", - }, - ) - raise e - except RetryError as e: - logger.exception(f"Failed after retries: {e.last_attempt.exception}") - raise e.last_attempt.exception from e - - -def retry_sync( - func: Callable[T_ParamSpec, T_Retval], - response_model: Type[T], - validation_context: dict, - args, - kwargs, - max_retries: int | Retrying = 1, - strict: Optional[bool] = None, - mode: Mode = Mode.TOOLS, -): - total_usage = CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0) - - # If max_retries is int, then create a Retrying object - if isinstance(max_retries, int): - logger.debug(f"max_retries: {max_retries}") - max_retries: Retrying = Retrying( - stop=stop_after_attempt(max_retries), - reraise=True, - ) - if not isinstance(max_retries, (Retrying, AsyncRetrying)): - raise ValueError("max_retries must be an int or a `tenacity.Retrying` object") - - try: - for attempt in max_retries: - with attempt: - try: - response = func(*args, **kwargs) - stream = kwargs.get("stream", False) - if ( - isinstance(response, ChatCompletion) - and response.usage is not None - ): - total_usage.completion_tokens += ( - response.usage.completion_tokens or 0 - ) - total_usage.prompt_tokens += response.usage.prompt_tokens or 0 - total_usage.total_tokens += response.usage.total_tokens or 0 - response.usage = total_usage # Replace each response usage with the total usage - return process_response( - response, - response_model=response_model, - stream=stream, - validation_context=validation_context, - strict=strict, - mode=mode, - ) - except (ValidationError, JSONDecodeError) as e: - logger.debug(f"Error response: {response}") - kwargs["messages"].append(dump_message(response.choices[0].message)) - # ! How do we handle this for parallel tools in the future? - if mode == Mode.TOOLS: - kwargs["messages"].append( - { - "role": "tool", - "tool_call_id": response.choices[0] - .message.tool_calls[0] - .id, - "name": response.choices[0] - .message.tool_calls[0] - .function.name, - "content": f"Recall the function correctly, fix the errors and exceptions found\n{e}", - } - ) - else: - kwargs["messages"].append( - { - "role": "user", - "content": f"Recall the function correctly, fix the errors and exceptions found\n{e}", - } - ) - if mode == Mode.MD_JSON: - kwargs["messages"].append( - { - "role": "assistant", - "content": "```json", - }, - ) - raise e - except RetryError as e: - logger.exception(f"Failed after retries: {e.last_attempt.exception}") - raise e.last_attempt.exception from e - - -def is_async(func: Callable) -> bool: - """Returns true if the callable is async, accounting for wrapped callables""" - is_coroutine = inspect.iscoroutinefunction(func) - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - is_coroutine = is_coroutine or inspect.iscoroutinefunction(func) - return is_coroutine - - -OVERRIDE_DOCS = """ -Creates a new chat completion for the provided messages and parameters. - -See: https://platform.openai.com/docs/api-reference/chat-completions/create - -Additional Notes: - -Using the `response_model` parameter, you can specify a response model to use for parsing the response from OpenAI's API. If its present, the response will be parsed using the response model, otherwise it will be returned as is. - -If `stream=True` is specified, the response will be parsed using the `from_stream_response` method of the response model, if available, otherwise it will be parsed using the `from_response` method. - -If need to obtain the raw response from OpenAI's API, you can access it using the `_raw_response` attribute of the response model. The `_raw_response.usage` attribute is modified to reflect the token usage from the last successful response as well as from any previous unsuccessful attempts. - -Parameters: - response_model (Union[Type[BaseModel], Type[OpenAISchema]]): The response model to use for parsing the response from OpenAI's API, if available (default: None) - max_retries (int): The maximum number of retries to attempt if the response is not valid (default: 0) - validation_context (dict): The validation context to use for validating the response (default: None) -""" class InstructorChatCompletionCreate(Protocol): def __call__( self, - response_model: Type[T_Model] = None, + response_model: type[T_Model] = None, validation_context: dict = None, max_retries: int = 1, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs, - ) -> T_Model: - ... + ) -> T_Model: ... + + +class AsyncInstructorChatCompletionCreate(Protocol): + async def __call__( + self, + response_model: type[T_Model] = None, + validation_context: dict = None, + max_retries: int = 1, + *args: T_ParamSpec.args, + **kwargs: T_ParamSpec.kwargs, + ) -> T_Model: ... @overload def patch( client: OpenAI, mode: Mode = Mode.TOOLS, -) -> OpenAI: - ... +) -> OpenAI: ... @overload def patch( client: AsyncOpenAI, mode: Mode = Mode.TOOLS, -) -> AsyncOpenAI: - ... +) -> AsyncOpenAI: ... @overload def patch( create: Callable[T_ParamSpec, T_Retval], mode: Mode = Mode.TOOLS, -) -> InstructorChatCompletionCreate: - ... +) -> InstructorChatCompletionCreate: ... + + +@overload +def patch( + create: Awaitable[T_Retval], + mode: Mode = Mode.TOOLS, +) -> InstructorChatCompletionCreate: ... def patch( @@ -559,9 +106,10 @@ def patch( @wraps(func) async def new_create_async( - response_model: Type[T_Model] = None, + response_model: type[T_Model] = None, validation_context: dict = None, max_retries: int = 1, + strict: bool = True, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs, ) -> T_Model: @@ -575,15 +123,17 @@ async def new_create_async( max_retries=max_retries, args=args, kwargs=new_kwargs, + strict=strict, mode=mode, - ) # type: ignore + ) return response @wraps(func) def new_create_sync( - response_model: Type[T_Model] = None, + response_model: type[T_Model] = None, validation_context: dict = None, max_retries: int = 1, + strict: bool = True, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs, ) -> T_Model: @@ -596,13 +146,13 @@ def new_create_sync( validation_context=validation_context, max_retries=max_retries, args=args, + strict=strict, kwargs=new_kwargs, mode=mode, ) return response new_create = new_create_async if func_is_async else new_create_sync - new_create.__doc__ = OVERRIDE_DOCS if client is not None: client.chat.completions.create = new_create @@ -611,7 +161,7 @@ def new_create_sync( return new_create -def apatch(client: AsyncOpenAI, mode: Mode = Mode.TOOLS): +def apatch(client: AsyncOpenAI, mode: Mode = Mode.TOOLS) -> AsyncOpenAI: """ No longer necessary, use `patch` instead. diff --git a/instructor/process_response.py b/instructor/process_response.py new file mode 100644 index 000000000..16c24ad94 --- /dev/null +++ b/instructor/process_response.py @@ -0,0 +1,363 @@ +# type: ignore[all] +from __future__ import annotations + +from collections.abc import Iterable +from textwrap import dedent +from instructor.dsl.iterable import IterableBase, IterableModel +from instructor.dsl.parallel import ParallelBase, ParallelModel, handle_parallel_model +from instructor.dsl.partial import PartialBase +from instructor.dsl.simple_type import AdapterBase, ModelAdapter, is_simple_type +from instructor.function_calls import OpenAISchema, openai_schema +from instructor.utils import merge_consecutive_messages +from openai.types.chat import ChatCompletion +from pydantic import BaseModel + +import json +import inspect +import logging +from typing import ( + get_args, + get_origin, + TypeVar, + Any, +) +from collections.abc import Generator +from typing_extensions import ParamSpec + +from instructor.mode import Mode + +logger = logging.getLogger("instructor") + +T_Model = TypeVar("T_Model", bound=BaseModel) +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") +T = TypeVar("T") + + +async def process_response_async( + response: ChatCompletion, + *, + response_model: type[T_Model | OpenAISchema | BaseModel] | None, + stream: bool = False, + validation_context: dict[str, Any] | None = None, + strict: bool | None = None, + mode: Mode = Mode.TOOLS, +) -> T_Model | ChatCompletion: + """Processes a OpenAI response with the response model, if available. + It can use `validation_context` and `strict` to validate the response + via the pydantic model + + Args: + response (ChatCompletion): The response from OpenAI's API + response_model (BaseModel): The response model to use for parsing the response + stream (bool): Whether the response is a stream + validation_context (dict, optional): The validation context to use for validating the response. Defaults to None. + strict (bool, optional): Whether to use strict json parsing. Defaults to None. + """ + + logger.debug( + f"Instructor Raw Response: {response}", + ) + if response_model is None: + return response + + if ( + inspect.isclass(response_model) + and issubclass(response_model, (IterableBase, PartialBase)) + and stream + ): + model = await response_model.from_streaming_response_async( + response, + mode=mode, + ) + return model + + model = response_model.from_response( + response, + validation_context=validation_context, + strict=strict, + mode=mode, + ) + + # ? This really hints at the fact that we need a better way of + # ? attaching usage data and the raw response to the model we return. + if isinstance(model, IterableBase): + logger.debug(f"Returning takes from IterableBase") + return [task for task in model.tasks] + + if isinstance(response_model, ParallelBase): + logger.debug(f"Returning model from ParallelBase") + return model + + if isinstance(model, AdapterBase): + logger.debug(f"Returning model from AdapterBase") + return model.content + + model._raw_response = response + return model + + +def process_response( + response: T_Model, + *, + response_model: type[OpenAISchema | BaseModel], + stream: bool, + validation_context: dict | None = None, + strict=None, + mode: Mode = Mode.TOOLS, +) -> T_Model | Generator[T_Model, None, None] | ChatCompletion: + """Processes a OpenAI response with the response model, if available. + + Args: + response (T): The response from OpenAI's API + response_model (Type[T_Model]): The response model to use for parsing the response + stream (bool): Whether the response is a stream + validation_context (dict, optional): The validation context to use for validating the response. Defaults to None. + strict (_type_, optional): Whether to use strict json parsing. Defaults to None. + mode (Mode, optional): The openai completion mode. Defaults to Mode.FUNCTIONS. + + Returns: + Union[T_Model, T]: The parsed response, if a response model is available, otherwise the response as is from the SDK + """ + + logger.debug( + f"Instructor Raw Response: {response}", + ) + + if response_model is None: + logger.debug("No response model, returning response as is") + return response + + if ( + inspect.isclass(response_model) + and issubclass(response_model, (IterableBase, PartialBase)) + and stream + ): + model = response_model.from_streaming_response( + response, + mode=mode, + ) + return model + + model = response_model.from_response( + response, + validation_context=validation_context, + strict=strict, + mode=mode, + ) + + # ? This really hints at the fact that we need a better way of + # ? attaching usage data and the raw response to the model we return. + if isinstance(model, IterableBase): + logger.debug(f"Returning takes from IterableBase") + return [task for task in model.tasks] + + if isinstance(response_model, ParallelBase): + logger.debug(f"Returning model from ParallelBase") + return model + + if isinstance(model, AdapterBase): + logger.debug(f"Returning model from AdapterBase") + return model.content + + model._raw_response = response + return model + + +def handle_response_model( + response_model: type[T] | None, mode: Mode = Mode.TOOLS, **kwargs: Any +) -> tuple[type[T], dict[str, Any]]: + """Prepare the response model type hint, and returns the response_model + along with the new modified kwargs needed to be able to use the response_model + parameter with the patch function. + + + Args: + response_model (T): The response model to use for parsing the response + mode (Mode, optional): The openai completion mode. Defaults to Mode.TOOLS. + + Raises: + NotImplementedError: When using stream=True with a non-iterable response_model + ValueError: When using an invalid patch mode + + Returns: + Union[Type[OpenAISchema], dict]: The response model to use for parsing the response + """ + new_kwargs = kwargs.copy() + if response_model is not None: + # Handles the case where the response_model is a simple type + # Literal, Annotated, Union, str, int, float, bool, Enum + # We wrap the response_model in a ModelAdapter that sets 'content' as the response + if is_simple_type(response_model): + response_model = ModelAdapter[response_model] + + # This a special case for parallel tools + if mode == Mode.PARALLEL_TOOLS: + assert ( + new_kwargs.get("stream", False) is False + ), "stream=True is not supported when using PARALLEL_TOOLS mode" + new_kwargs["tools"] = handle_parallel_model(response_model) + new_kwargs["tool_choice"] = "auto" + + # This is a special case for parallel models + response_model = ParallelModel(typehint=response_model) + return response_model, new_kwargs + + # This is for all other single model cases + if get_origin(response_model) is Iterable: + iterable_element_class = get_args(response_model)[0] + response_model = IterableModel(iterable_element_class) + if not issubclass(response_model, OpenAISchema): + response_model = openai_schema(response_model) # type: ignore + + if new_kwargs.get("stream", False) and not issubclass( + response_model, (IterableBase, PartialBase) + ): + raise NotImplementedError( + "stream=True is not supported when using response_model parameter for non-iterables" + ) + + if mode == Mode.FUNCTIONS: + new_kwargs["functions"] = [response_model.openai_schema] # type: ignore + new_kwargs["function_call"] = {"name": response_model.openai_schema["name"]} # type: ignore + elif mode in {Mode.TOOLS, Mode.MISTRAL_TOOLS}: + new_kwargs["tools"] = [ + { + "type": "function", + "function": response_model.openai_schema, + } + ] + if mode == Mode.MISTRAL_TOOLS: + new_kwargs["tool_choice"] = "any" + else: + new_kwargs["tool_choice"] = { + "type": "function", + "function": {"name": response_model.openai_schema["name"]}, + } + elif mode in {Mode.JSON, Mode.MD_JSON, Mode.JSON_SCHEMA}: + # If its a JSON Mode we need to massage the prompt a bit + # in order to get the response we want in a json format + message = dedent( + f""" + As a genius expert, your task is to understand the content and provide + the parsed objects in json that match the following json_schema:\n + + {json.dumps(response_model.model_json_schema(), indent=2)} + + Make sure to return an instance of the JSON, not the schema itself + """ + ) + + if mode == Mode.JSON: + new_kwargs["response_format"] = {"type": "json_object"} + + elif mode == Mode.JSON_SCHEMA: + new_kwargs["response_format"] = { + "type": "json_object", + "schema": response_model.model_json_schema(), + } + + elif mode == Mode.MD_JSON: + new_kwargs["messages"].append( + { + "role": "user", + "content": "Return the correct JSON response within a ```json codeblock. not the JSON_SCHEMA", + }, + ) + # check that the first message is a system message + # if it is not, add a system message to the beginning + if new_kwargs["messages"][0]["role"] != "system": + new_kwargs["messages"].insert( + 0, + { + "role": "system", + "content": message, + }, + ) + # if it is, system append the schema to the end + else: + new_kwargs["messages"][0]["content"] += f"\n\n{message}" + elif mode == Mode.ANTHROPIC_TOOLS: + tool_descriptions = response_model.anthropic_schema + new_kwargs["tools"] = [tool_descriptions] + + system_messages = [ + m["content"] for m in new_kwargs["messages"] if m["role"] == "system" + ] + new_kwargs["system"] = "\n\n".join(system_messages) + new_kwargs["messages"] = [ + m for m in new_kwargs["messages"] if m["role"] != "system" + ] + + elif mode == Mode.ANTHROPIC_JSON: + # anthropic wants system message to be a string so we first extract out any system message + openai_system_messages = [ + message["content"] + for message in new_kwargs.get("messages", []) + if message["role"] == "system" + ] + + new_kwargs["system"] = ( + new_kwargs.get("system", "") + + "\n\n" + + "\n\n".join(openai_system_messages) + ) + + new_kwargs["system"] += f""" + You must only response in JSON format that adheres to the following schema: + + + {json.dumps(response_model.model_json_schema(), indent=2)} + + """ + new_kwargs["system"] = dedent(new_kwargs["system"]) + + new_kwargs["messages"] = [ + message + for message in new_kwargs.get("messages", []) + if message["role"] != "system" + ] + + # the messages array must be alternating roles of user and assistant, we must merge + # consecutive user messages into a single message + new_kwargs["messages"] = merge_consecutive_messages(new_kwargs["messages"]) + + elif mode == Mode.COHERE_TOOLS: + instruction = f"""\ +Extract a valid {response_model.__name__} object based on the chat history and the json schema below. +{response_model.model_json_schema()} +The JSON schema was obtained by running: +```python +schema = {response_model.__name__}.model_json_schema() +``` + +The output must be a valid JSON object that `{response_model.__name__}.model_validate_json()` can successfully parse. +""" + messages = new_kwargs.pop("messages", []) + chat_history = [] + for message in messages: + # format in Cohere's ChatMessage format + chat_history.append( + { + "role": message["role"], + "message": message["content"], + } + ) + new_kwargs["message"] = instruction + new_kwargs["chat_history"] = chat_history + else: + raise ValueError(f"Invalid patch mode: {mode}") + + logger.debug( + f"Instructor Request: {mode.value=}, {response_model=}, {new_kwargs=}", + extra={ + "mode": mode.value, + "response_model": ( + response_model.__name__ + if response_model is not None and hasattr(response_model, "__name__") + else str(response_model) + ), + "new_kwargs": new_kwargs, + }, + ) + return response_model, new_kwargs diff --git a/instructor/retry.py b/instructor/retry.py new file mode 100644 index 000000000..0524ec93b --- /dev/null +++ b/instructor/retry.py @@ -0,0 +1,256 @@ +# type: ignore[all] +from __future__ import annotations + +import logging + +from openai.types.chat import ChatCompletion +from instructor.mode import Mode +from instructor.process_response import process_response, process_response_async +from instructor.utils import ( + dump_message, + update_total_usage, + merge_consecutive_messages, +) + +from openai.types.completion_usage import CompletionUsage +from pydantic import ValidationError +from tenacity import AsyncRetrying, RetryError, Retrying, stop_after_attempt + + +from json import JSONDecodeError +from pydantic import BaseModel +from typing import Callable, TypeVar, Any +from typing_extensions import ParamSpec + +logger = logging.getLogger("instructor") + +T_Model = TypeVar("T_Model", bound=BaseModel) +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") +T = TypeVar("T") + + +class InstructorRetryException(Exception): + def __init__( + self, + *args, + last_completion, + messages: list, + n_attempts: int, + total_usage, + **kwargs, + ): + self.last_completion = last_completion + self.messages = messages + self.n_attempts = n_attempts + self.total_usage = total_usage + super().__init__(*args, **kwargs) + + +def reask_messages(response: ChatCompletion, mode: Mode, exception: Exception): + if mode == Mode.ANTHROPIC_TOOLS: + # The original response + assistant_content = [] + tool_use_id = None + for content in response.content: + assistant_content.append(content.model_dump()) + # Assuming exception from single tool invocation + if ( + content.type == "tool_use" + and isinstance(exception, ValidationError) + and content.name == exception.title + ): + tool_use_id = content.id + + yield { + "role": "assistant", + "content": assistant_content, + } + if tool_use_id is not None: + yield { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": f"Validation Error found:\n{exception}\nRecall the function correctly, fix the errors", + "is_error": True, + } + ], + } + else: + yield { + "role": "user", + "content": f"Validation Error due to no tool invocation:\n{exception}\nRecall the function correctly, fix the errors", + } + return + if mode == Mode.ANTHROPIC_JSON: + from anthropic.types import Message + + assert isinstance(response, Message) + yield { + "role": "user", + "content": f"""Validation Errors found:\n{exception}\nRecall the function correctly, fix the errors found in the following attempt:\n{response.content[0].text}""", + } + return + if mode == Mode.COHERE_TOOLS: + yield { + "role": "user", + "content": f"Validation Error found:\n{exception}\nRecall the function correctly, fix the errors", + } + return + + yield dump_message(response.choices[0].message) + # TODO: Give users more control on configuration + if mode == Mode.TOOLS: + for tool_call in response.choices[0].message.tool_calls: + yield { + "role": "tool", + "tool_call_id": tool_call.id, + "name": tool_call.function.name, + "content": f"Validation Error found:\n{exception}\nRecall the function correctly, fix the errors", + } + elif mode == Mode.MD_JSON: + yield { + "role": "user", + "content": f"Correct your JSON ONLY RESPONSE, based on the following errors:\n{exception}", + } + else: + yield { + "role": "user", + "content": f"Recall the function correctly, fix the errors, exceptions found\n{exception}", + } + + +def retry_sync( + func: Callable[T_ParamSpec, T_Retval], + response_model: type[T_Model], + validation_context: dict, + args, + kwargs, + max_retries: int | Retrying = 1, + strict: bool | None = None, + mode: Mode = Mode.TOOLS, +) -> T_Model: + total_usage = CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0) + if mode in {Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_JSON}: + from anthropic.types import Usage as AnthropicUsage + + total_usage = AnthropicUsage(input_tokens=0, output_tokens=0) + + # If max_retries is int, then create a Retrying object + if isinstance(max_retries, int): + logger.debug(f"max_retries: {max_retries}") + max_retries = Retrying( + stop=stop_after_attempt(max_retries), + reraise=True, + ) + if not isinstance(max_retries, (Retrying, AsyncRetrying)): + raise ValueError("max_retries must be an int or a `tenacity.Retrying` object") + + try: + for attempt in max_retries: + with attempt: + try: + response = func(*args, **kwargs) + stream = kwargs.get("stream", False) + response = update_total_usage(response, total_usage) + return process_response( + response, + response_model=response_model, + stream=stream, + validation_context=validation_context, + strict=strict, + mode=mode, + ) + except (ValidationError, JSONDecodeError) as e: + logger.debug(f"Error response: {response}") + kwargs["messages"].extend(reask_messages(response, mode, e)) + if mode in {Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_JSON}: + kwargs["messages"] = merge_consecutive_messages( + kwargs["messages"] + ) + raise InstructorRetryException( + e, + last_completion=response, + n_attempts=attempt.retry_state.attempt_number, + messages=kwargs["messages"], + total_usage=total_usage, + ) from e + except RetryError as e: + raise InstructorRetryException( + e, + last_completion=response, + n_attempts=attempt.retry_state.attempt_number, + messages=kwargs["messages"], + total_usage=total_usage, + ) from e + + +async def retry_async( + func: Callable[T_ParamSpec, T_Retval], + response_model: type[T] | None, + validation_context: dict[str, Any] | None, + args: Any, + kwargs: Any, + max_retries: int | AsyncRetrying = 1, + strict: bool | None = None, + mode: Mode = Mode.TOOLS, +) -> T: + total_usage = CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0) + if mode in {Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_JSON}: + from anthropic.types import Usage as AnthropicUsage + + total_usage = AnthropicUsage(input_tokens=0, output_tokens=0) + + # If max_retries is int, then create a AsyncRetrying object + if isinstance(max_retries, int): + logger.debug(f"max_retries: {max_retries}") + max_retries = AsyncRetrying( + stop=stop_after_attempt(max_retries), + reraise=True, + ) + if not isinstance(max_retries, (AsyncRetrying, Retrying)): + raise ValueError( + "max_retries must be an `int` or a `tenacity.AsyncRetrying` object" + ) + + try: + async for attempt in max_retries: + logger.debug(f"Retrying, attempt: {attempt}") + with attempt: + try: + response: ChatCompletion = await func(*args, **kwargs) + stream = kwargs.get("stream", False) + response = update_total_usage(response, total_usage) + return await process_response_async( + response, + response_model=response_model, + stream=stream, + validation_context=validation_context, + strict=strict, + mode=mode, + ) + except (ValidationError, JSONDecodeError) as e: + logger.debug(f"Error response: {response}", e) + kwargs["messages"].extend(reask_messages(response, mode, e)) + if mode in {Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_JSON}: + kwargs["messages"] = merge_consecutive_messages( + kwargs["messages"] + ) + raise InstructorRetryException( + e, + last_completion=response, + n_attempts=attempt.retry_state.attempt_number, + messages=kwargs["messages"], + total_usage=total_usage, + ) from e + except RetryError as e: + logger.exception(f"Failed after retries: {e.last_attempt.exception}") + raise InstructorRetryException( + e, + last_completion=response, + n_attempts=attempt.retry_state.attempt_number, + messages=kwargs["messages"], + total_usage=total_usage, + ) from e diff --git a/instructor/utils.py b/instructor/utils.py new file mode 100644 index 000000000..9f9448ed4 --- /dev/null +++ b/instructor/utils.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import inspect +import json +import logging +from collections.abc import AsyncGenerator, Generator, Iterable +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + Protocol, + TypeVar, +) + +from openai.types import CompletionUsage as OpenAIUsage +from openai.types.chat import ( + ChatCompletion, + ChatCompletionMessage, + ChatCompletionMessageParam, +) + +if TYPE_CHECKING: + from anthropic.types import Usage as AnthropicUsage + + +logger = logging.getLogger("instructor") +R_co = TypeVar("R_co", covariant=True) +T_Model = TypeVar("T_Model", bound="Response") + +from enum import Enum + + +class Response(Protocol): + usage: OpenAIUsage | AnthropicUsage + + +class Provider(Enum): + OPENAI = "openai" + ANTHROPIC = "anthropic" + ANYSCALE = "anyscale" + TOGETHER = "together" + GROQ = "groq" + MISTRAL = "mistral" + COHERE = "cohere" + UNKNOWN = "unknown" + + +def get_provider(base_url: str) -> Provider: + if "anyscale" in str(base_url): + return Provider.ANYSCALE + elif "together" in str(base_url): + return Provider.TOGETHER + elif "anthropic" in str(base_url): + return Provider.ANTHROPIC + elif "groq" in str(base_url): + return Provider.GROQ + elif "openai" in str(base_url): + return Provider.OPENAI + elif "mistral" in str(base_url): + return Provider.MISTRAL + elif "cohere" in str(base_url): + return Provider.COHERE + return Provider.UNKNOWN + + +def extract_json_from_codeblock(content: str) -> str: + first_paren = content.find("{") + last_paren = content.rfind("}") + return content[first_paren : last_paren + 1] + + +def extract_json_from_stream(chunks: Iterable[str]) -> Generator[str, None, None]: + capturing = False + brace_count = 0 + for chunk in chunks: + for char in chunk: + if char == "{": + capturing = True + brace_count += 1 + yield char + elif char == "}" and capturing: + brace_count -= 1 + yield char + if brace_count == 0: + capturing = False + break # Cease yielding upon closing the current JSON object + elif capturing: + yield char + + +async def extract_json_from_stream_async( + chunks: AsyncGenerator[str, None], +) -> AsyncGenerator[str, None]: + capturing = False + brace_count = 0 + async for chunk in chunks: + for char in chunk: + if char == "{": + capturing = True + brace_count += 1 + yield char + elif char == "}" and capturing: + brace_count -= 1 + yield char + if brace_count == 0: + capturing = False + break # Cease yielding upon closing the current JSON object + elif capturing: + yield char + + +def update_total_usage( + response: T_Model, + total_usage: OpenAIUsage | AnthropicUsage, +) -> T_Model | ChatCompletion: + response_usage = getattr(response, "usage", None) + if isinstance(response_usage, OpenAIUsage) and isinstance(total_usage, OpenAIUsage): + total_usage.completion_tokens += response_usage.completion_tokens or 0 + total_usage.prompt_tokens += response_usage.prompt_tokens or 0 + total_usage.total_tokens += response_usage.total_tokens or 0 + response.usage = total_usage # Replace each response usage with the total usage + return response + + # Anthropic usage. + try: + from anthropic.types import Usage as AnthropicUsage + + if isinstance(response_usage, AnthropicUsage) and isinstance( + total_usage, AnthropicUsage + ): + total_usage.input_tokens += response_usage.input_tokens or 0 + total_usage.output_tokens += response_usage.output_tokens or 0 + response.usage = total_usage + return response + except ImportError: + pass + + logger.debug("No compatible response.usage found, token usage not updated.") + return response + + +def dump_message(message: ChatCompletionMessage) -> ChatCompletionMessageParam: + """Dumps a message to a dict, to be returned to the OpenAI API. + Workaround for an issue with the OpenAI API, where the `tool_calls` field isn't allowed to be present in requests + if it isn't used. + """ + ret: ChatCompletionMessageParam = { + "role": message.role, + "content": message.content or "", + } + if hasattr(message, "tool_calls") and message.tool_calls is not None: + ret["tool_calls"] = message.model_dump()["tool_calls"] + if ( + hasattr(message, "function_call") + and message.function_call is not None + and ret["content"] + ): + ret["content"] += json.dumps(message.model_dump()["function_call"]) + return ret + + +def is_async(func: Callable[..., Any]) -> bool: + """Returns true if the callable is async, accounting for wrapped callables""" + is_coroutine = inspect.iscoroutinefunction(func) + while hasattr(func, "__wrapped__"): + func = func.__wrapped__ # type: ignore - dynamic + is_coroutine = is_coroutine or inspect.iscoroutinefunction(func) + return is_coroutine + + +def merge_consecutive_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + # merge all consecutive user messages into a single message + new_messages: list[dict[str, Any]] = [] + for message in messages: + new_content = message["content"] + if isinstance(new_content, str): + new_content = [{"type": "text", "text": new_content}] + + if len(new_messages) > 0 and message["role"] == new_messages[-1]["role"]: + new_messages[-1]["content"].extend(new_content) + else: + new_messages.append( + { + "role": message["role"], + "content": new_content, + } + ) + + return new_messages + + +class classproperty(Generic[R_co]): + """Descriptor for class-level properties. + + Examples: + >>> from instructor.utils import classproperty + + >>> class MyClass: + ... @classproperty + ... def my_property(cls): + ... return cls + + >>> assert MyClass.my_property + """ + + def __init__(self, method: Callable[[Any], R_co]) -> None: + self.cproperty = method + + def __get__(self, instance: object, cls: type[Any]) -> R_co: + return self.cproperty(cls) diff --git a/mkdocs.yml b/mkdocs.yml index aa5df9e96..18ff10f30 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ site_author: Jason Liu site_description: A lightweight library for structured outputs with LLMs. repo_name: instructor repo_url: https://github.com/jxnl/instructor/ -site_url: https://jxnl.github.io/instructor/ +site_url: https://python.useinstructor.com/ edit_uri: edit/main/docs/ copyright: Copyright © 2024 Jason Liu theme: @@ -120,12 +120,34 @@ nav: - Introduction: - Welcome To Instructor: 'index.md' - Why use Instructor?: 'why.md' - - Prompting Tips: 'concepts/prompting.md' - Help with Instructor: 'help.md' - Installation: 'installation.md' - Contributing: 'contributing.md' - - Concepts: - Philosophy: 'concepts/philosophy.md' + - Cookbook: + - Cookbooks: 'examples/index.md' + - Text Classification: 'examples/classification.md' + - Batch Classification (User Defined): 'examples/batch_classification.md' + - LLM Self Critique: 'examples/self_critique.md' + - Extracting Tables with GPT-V: 'examples/extracting_tables.md' + - Extracting From Slides with GPT-V: 'examples/extract_slides.md' + - Content Moderation: 'examples/moderation.md' + - Citing Sources (RAG): 'examples/exact_citations.md' + - Extracting Knowledge Graphs: 'examples/knowledge_graph.md' + - Extracting Complex Entities: 'examples/entity_resolution.md' + - Expanding Search Queries (RAG): 'examples/search.md' + - Query Planning (RAG): 'examples/planning-tasks.md' + - PII Data Sanitization: 'examples/pii.md' + - Enabling Open Source Models: 'examples/open_source.md' + - Image to Ad Copy: 'examples/image_to_ad_copy.md' + - Ollama: 'examples/ollama.md' + - SQLModel Integration: 'examples/sqlmodel.md' + - Including Examples in Prompt: 'examples/examples.md' + - Document Segmentation: 'examples/document_segmentation.md' + - IBM watsonx.ai: 'examples/watsonx.md' + - Blog: + - "blog/index.md" + - Concepts: - Models: 'concepts/models.md' - Fields: 'concepts/fields.md' - Types: 'concepts/types.md' @@ -140,29 +162,12 @@ nav: - Raw Response: 'concepts/raw_response.md' - FastAPI: 'concepts/fastapi.md' - Caching: 'concepts/caching.md' + - Logging: 'concepts/logging.md' - Distillation: "concepts/distillation.md" - Union: 'concepts/union.md' - Alias: 'concepts/alias.md' - Enums: 'concepts/enums.md' - Type Adapter: 'concepts/typeadapter.md' - - Cookbook: - - Cookbooks: 'examples/index.md' - - Text Classification: 'examples/classification.md' - - Batch Classification (User Defined): 'examples/batch_classification.md' - - LLM Self Critique: 'examples/self_critique.md' - - Extracting Tables with GPT-V: 'examples/extracting_tables.md' - - Extracting From Slides with GPT-V: 'examples/extract_slides.md' - - Content Moderation: 'examples/moderation.md' - - Citing Sources (RAG): 'examples/exact_citations.md' - - Extracting Knowledge Graphs: 'examples/knowledge_graph.md' - - Extracting Complex Entities: 'examples/entity_resolution.md' - - Expanding Search Queries (RAG): 'examples/search.md' - - Query Planning (RAG): 'examples/planning-tasks.md' - - PII Data Sanitization: 'examples/pii.md' - - Enabling Open Source Models: 'examples/open_source.md' - - Image to Ad Copy: 'examples/image_to_ad_copy.md' - - Ollama: 'examples/ollama.md' - - SQLModel Integration: 'examples/sqlmodel.md' - Hub: - Introducing Instructor Hub: 'hub/index.md' - Single Classification Model: 'hub/single_classification.md' @@ -173,27 +178,27 @@ nav: - Using Llama CPP: 'hub/llama-cpp-python.md' - Using Together Compute: 'hub/together.md' - Using Anyscale: 'hub/anyscale.md' + - Using Groq: 'hub/groq.md' + - Using Mistral: 'hub/mistral.md' + - Using Cohere: 'hub/cohere.md' - Batch Async Classification w/ Langsmith: 'hub/batch_classification_langsmith.md' - Action Items: 'hub/action_items.md' - Partial Streaming: 'hub/partial_streaming.md' - Extract Contact Info: 'hub/extract_contact_info.md' - Using Mistral Large: 'hub/mistral.md' - Knowledge Graphs: 'hub/knowledge_graph.md' + - Extract Youtube Clips: "hub/youtube_clips.md" + - Knowledge Graphs: 'tutorials/5-knowledge-graphs.ipynb' + - CLI Reference: + - "CLI Reference": "cli/index.md" + - "Finetuning GPT-3.5": "cli/finetune.md" + - "Usage Tracking": "cli/usage.md" - Tutorials: - Tutorials (Notebooks): 'tutorials/1-introduction.ipynb' - Tips and Tricks: 'tutorials/2-tips.ipynb' - Applications RAG: 'tutorials/3-0-applications-rag.ipynb' - Applications RAG - 2: 'tutorials/3-1-validation-rag.ipynb' - Validation: 'tutorials/4-validation.ipynb' - - Knowledge Graphs: 'tutorials/5-knowledge-graphs.ipynb' - - CLI Reference: - - "CLI Reference": "cli/index.md" - - "Finetuning GPT-3.5": "cli/finetune.md" - - "Usage Tracking": "cli/usage.md" - - API Reference: - - 'Core Library': 'api.md' - - Blog: - - "blog/index.md" plugins: - mkdocs-jupyter: @@ -219,21 +224,6 @@ plugins: post_date_format: yyyy/MM/dd post_url_format: "{date}/{slug}" authors_file: "{blog}/.authors.yml" - - rss: - match_path: blog/posts/.* - date_from_meta: - as_creation: date - categories: - - categories - - tags - enabled: !ENV [CI, false] - - redirects: - redirect_maps: - 'blog/posts/ollama.md': 'hub/ollama.md' - 'blob/posts/llama-cpp-python.md': 'hub/llama-cpp-python.md' - 'blog/posts/together.md': 'hub/together.md' - 'blog/posts/anyscale.md': 'hub/anyscale.md' - 'examples/action_items.md': 'hub/action_items.md' extra: analytics: provider: google @@ -256,4 +246,4 @@ extra: - icon: fontawesome/brands/twitter link: https://twitter.com/jxnlco - icon: fontawesome/brands/github - link: https://github.com/jxnl \ No newline at end of file + link: https://github.com/jxnl diff --git a/poetry.lock b/poetry.lock index cf4746396..065075475 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,88 +1,88 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiohttp" -version = "3.9.3" +version = "3.9.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, ] [package.dependencies] @@ -121,6 +121,30 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "anthropic" +version = "0.23.1" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.7" +files = [ + {file = "anthropic-0.23.1-py3-none-any.whl", hash = "sha256:6dc5779dae83a5834864f4a4af0166c972b70f4cb8fd2765e1558282cc6d6242"}, + {file = "anthropic-0.23.1.tar.gz", hash = "sha256:9325103702cbc96bb09d1b58c36bde75c726f6a01029fb4d85f41ebba07e9066"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tokenizers = ">=0.13.0" +typing-extensions = ">=4.7,<5" + +[package.extras] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth (>=2,<3)"] + [[package]] name = "anyio" version = "4.3.0" @@ -239,33 +263,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "24.2.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -303,13 +327,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "cairocffi" -version = "1.6.1" +version = "1.7.0" description = "cffi-based cairo bindings for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cairocffi-1.6.1-py3-none-any.whl", hash = "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e"}, - {file = "cairocffi-1.6.1.tar.gz", hash = "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7"}, + {file = "cairocffi-1.7.0-py3-none-any.whl", hash = "sha256:1f29a8d41dbda4090c0aa33bcdea64f3b493e95f74a43ea107c4a8a7b7f632ef"}, + {file = "cairocffi-1.7.0.tar.gz", hash = "sha256:7761863603894305f3160eca68452f373433ca8745ab7dd445bd2c6ce50dcab7"}, ] [package.dependencies] @@ -317,7 +341,7 @@ cffi = ">=1.1.0" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "numpy", "pikepdf", "pytest"] +test = ["numpy", "pikepdf", "pytest", "ruff"] xcb = ["xcffib (>=1.4.0)"] [[package]] @@ -530,6 +554,27 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "cohere" +version = "5.3.3" +description = "" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "cohere-5.3.3-py3-none-any.whl", hash = "sha256:686db9d7c0b858be8c1da173fbe77dfda377fd794e221e473b654440e81336cd"}, + {file = "cohere-5.3.3.tar.gz", hash = "sha256:fbf59c0bab0ded4d28091fa0c193a116d8303c4c0b09a42721dd8a1230ea27c3"}, +] + +[package.dependencies] +fastavro = ">=1.9.4,<2.0.0" +httpx = ">=0.21.2" +httpx-sse = ">=0.4.0,<0.5.0" +pydantic = ">=1.9.2" +requests = ">=2.0.0,<3.0.0" +tokenizers = ">=0.19,<0.20" +types-requests = ">=2.0.0,<3.0.0" +typing_extensions = ">=4.0.0" + [[package]] name = "colorama" version = "0.4.6" @@ -543,13 +588,13 @@ files = [ [[package]] name = "comm" -version = "0.2.1" +version = "0.2.2" description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." optional = false python-versions = ">=3.8" files = [ - {file = "comm-0.2.1-py3-none-any.whl", hash = "sha256:87928485c0dfc0e7976fd89fc1e187023cf587e7c353e4a9b417555b44adf021"}, - {file = "comm-0.2.1.tar.gz", hash = "sha256:0bc91edae1344d39d3661dcbc36937181fdaddb304790458f8b044dbc064b89a"}, + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, ] [package.dependencies] @@ -560,63 +605,63 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.4.3" +version = "7.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.extras] @@ -728,24 +773,24 @@ files = [ [[package]] name = "docstring-parser" -version = "0.15" +version = "0.16" description = "Parse Python docstrings in reST, Google and Numpydoc format" optional = false python-versions = ">=3.6,<4.0" files = [ - {file = "docstring_parser-0.15-py3-none-any.whl", hash = "sha256:d1679b86250d269d06a99670924d6bce45adc00b08069dae8c47d98e89b667a9"}, - {file = "docstring_parser-0.15.tar.gz", hash = "sha256:48ddc093e8b1865899956fcc03b03e66bb7240c310fac5af81814580c55bf682"}, + {file = "docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637"}, + {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, ] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -784,6 +829,52 @@ typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastavro" +version = "1.9.4" +description = "Fast read/write of AVRO files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastavro-1.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:60cb38f07462a7fb4e4440ed0de67d3d400ae6b3d780f81327bebde9aa55faef"}, + {file = "fastavro-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:063d01d197fc929c20adc09ca9f0ca86d33ac25ee0963ce0b438244eee8315ae"}, + {file = "fastavro-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a9053fcfbc895f2a16a4303af22077e3a8fdcf1cd5d6ed47ff2ef22cbba2f0"}, + {file = "fastavro-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:02bf1276b7326397314adf41b34a4890f6ffa59cf7e0eb20b9e4ab0a143a1598"}, + {file = "fastavro-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56bed9eca435389a8861e6e2d631ec7f8f5dda5b23f93517ac710665bd34ca29"}, + {file = "fastavro-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:0cd2099c8c672b853e0b20c13e9b62a69d3fbf67ee7c59c7271ba5df1680310d"}, + {file = "fastavro-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af8c6d8c43a02b5569c093fc5467469541ac408c79c36a5b0900d3dd0b3ba838"}, + {file = "fastavro-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a138710bd61580324d23bc5e3df01f0b82aee0a76404d5dddae73d9e4c723f"}, + {file = "fastavro-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:903d97418120ca6b6a7f38a731166c1ccc2c4344ee5e0470d09eb1dc3687540a"}, + {file = "fastavro-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c443eeb99899d062dbf78c525e4614dd77e041a7688fa2710c224f4033f193ae"}, + {file = "fastavro-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac26ab0774d1b2b7af6d8f4300ad20bbc4b5469e658a02931ad13ce23635152f"}, + {file = "fastavro-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:cf7247874c22be856ba7d1f46a0f6e0379a6025f1a48a7da640444cbac6f570b"}, + {file = "fastavro-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:68912f2020e1b3d70557260b27dd85fb49a4fc6bfab18d384926127452c1da4c"}, + {file = "fastavro-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6925ce137cdd78e109abdb0bc33aad55de6c9f2d2d3036b65453128f2f5f5b92"}, + {file = "fastavro-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b928cd294e36e35516d0deb9e104b45be922ba06940794260a4e5dbed6c192a"}, + {file = "fastavro-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:90c9838bc4c991ffff5dd9d88a0cc0030f938b3fdf038cdf6babde144b920246"}, + {file = "fastavro-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:eca6e54da571b06a3c5a72dbb7212073f56c92a6fbfbf847b91c347510f8a426"}, + {file = "fastavro-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4b02839ac261100cefca2e2ad04cdfedc556cb66b5ec735e0db428e74b399de"}, + {file = "fastavro-1.9.4-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4451ee9a305a73313a1558d471299f3130e4ecc10a88bf5742aa03fb37e042e6"}, + {file = "fastavro-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8524fccfb379565568c045d29b2ebf71e1f2c0dd484aeda9fe784ef5febe1a8"}, + {file = "fastavro-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d0a00a6e09baa20f6f038d7a2ddcb7eef0e7a9980e947a018300cb047091b8"}, + {file = "fastavro-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23d7e5b29c9bf6f26e8be754b2c8b919838e506f78ef724de7d22881696712fc"}, + {file = "fastavro-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e6ab3ee53944326460edf1125b2ad5be2fadd80f7211b13c45fa0c503b4cf8d"}, + {file = "fastavro-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:64d335ec2004204c501f8697c385d0a8f6b521ac82d5b30696f789ff5bc85f3c"}, + {file = "fastavro-1.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:7e05f44c493e89e73833bd3ff3790538726906d2856f59adc8103539f4a1b232"}, + {file = "fastavro-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:253c63993250bff4ee7b11fb46cf3a4622180a783bedc82a24c6fdcd1b10ca2a"}, + {file = "fastavro-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d6942eb1db14640c2581e0ecd1bbe0afc8a83731fcd3064ae7f429d7880cb7"}, + {file = "fastavro-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d47bb66be6091cd48cfe026adcad11c8b11d7d815a2949a1e4ccf03df981ca65"}, + {file = "fastavro-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c293897f12f910e58a1024f9c77f565aa8e23b36aafda6ad8e7041accc57a57f"}, + {file = "fastavro-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:f05d2afcb10a92e2a9e580a3891f090589b3e567fdc5641f8a46a0b084f120c3"}, + {file = "fastavro-1.9.4.tar.gz", hash = "sha256:56b8363e360a1256c94562393dc7f8611f3baf2b3159f64fb2b9c6b87b14e876"}, +] + +[package.extras] +codecs = ["cramjam", "lz4", "zstandard"] +lz4 = ["lz4"] +snappy = ["cramjam"] +zstandard = ["zstandard"] + [[package]] name = "fastjsonschema" version = "2.19.1" @@ -798,6 +889,22 @@ files = [ [package.extras] devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] +[[package]] +name = "filelock" +version = "3.14.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -884,6 +991,41 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "fsspec" +version = "2024.3.1" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, + {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + [[package]] name = "ghp-import" version = "2.1.0" @@ -917,35 +1059,55 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.42" +version = "3.1.43" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, - {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] name = "griffe" -version = "0.41.0" +version = "0.44.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.41.0-py3-none-any.whl", hash = "sha256:8aa7fc6eb00cb80af9c0198178c6b7110cb59fa2c5187bb13ea25eebbe4dd928"}, - {file = "griffe-0.41.0.tar.gz", hash = "sha256:850128c3198c18713eaf0a6cc8572e590a16b1965f72a4e871e66cf84740903f"}, + {file = "griffe-0.44.0-py3-none-any.whl", hash = "sha256:8a4471c469ba980b87c843f1168850ce39d0c1d0c7be140dca2480f76c8e5446"}, + {file = "griffe-0.44.0.tar.gz", hash = "sha256:34aee1571042f9bf00529bc715de4516fb6f482b164e90d030300601009e0223"}, ] [package.dependencies] colorama = ">=0.4" +[[package]] +name = "groq" +version = "0.4.2" +description = "The official Python library for the groq API" +optional = false +python-versions = ">=3.7" +files = [ + {file = "groq-0.4.2-py3-none-any.whl", hash = "sha256:5b2b472c64d9f35210e0487db465415d47162da3a114031ecbfc8843d26302a5"}, + {file = "groq-0.4.2.tar.gz", hash = "sha256:42e8b0abd0f2b2da024b9a747d28960d62951a5364f078e1537c9fceeca8259d"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.7,<5" + [[package]] name = "h11" version = "0.14.0" @@ -969,13 +1131,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.4" +version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, - {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, ] [package.dependencies] @@ -986,17 +1148,17 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.25.0)"] +trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.25.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] [package.dependencies] @@ -1012,17 +1174,81 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + +[[package]] +name = "huggingface-hub" +version = "0.22.2" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"}, + {file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +inference = ["aiohttp", "minijinja (>=1.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1036,13 +1262,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.2" +version = "6.29.4" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.2-py3-none-any.whl", hash = "sha256:50384f5c577a260a1d53f1f59a828c7266d321c9b7d00d345693783f66616055"}, - {file = "ipykernel-6.29.2.tar.gz", hash = "sha256:3bade28004e3ff624ed57974948116670604ac5f676d12339693f3142176d3f0"}, + {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, + {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, ] [package.dependencies] @@ -1065,17 +1291,17 @@ cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] pyqt5 = ["pyqt5"] pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (==0.23.4)", "pytest-cov", "pytest-timeout"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] [[package]] name = "ipython" -version = "8.22.1" +version = "8.18.1" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "ipython-8.22.1-py3-none-any.whl", hash = "sha256:869335e8cded62ffb6fac8928e5287a05433d6462e3ebaac25f4216474dd6bc4"}, - {file = "ipython-8.22.1.tar.gz", hash = "sha256:39c6f9efc079fb19bfb0f17eee903978fe9a290b1b82d68196c641cecb76ea22"}, + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] [package.dependencies] @@ -1084,24 +1310,25 @@ decorator = "*" exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" -traitlets = ">=5.13.0" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] -all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] [[package]] name = "jedi" @@ -1186,16 +1413,17 @@ referencing = ">=0.31.0" [[package]] name = "jupyter-client" -version = "8.6.0" +version = "8.6.1" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_client-8.6.0-py3-none-any.whl", hash = "sha256:909c474dbe62582ae62b758bca86d6518c85234bdee2d908c778db6d72f39d99"}, - {file = "jupyter_client-8.6.0.tar.gz", hash = "sha256:0642244bb83b4764ae60d07e010e15f0e2d275ec4e918a8f7b80fbbef3ca60c7"}, + {file = "jupyter_client-8.6.1-py3-none-any.whl", hash = "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f"}, + {file = "jupyter_client-8.6.1.tar.gz", hash = "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f"}, ] [package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -1208,13 +1436,13 @@ test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pyt [[package]] name = "jupyter-core" -version = "5.7.1" +version = "5.7.2" description = "Jupyter core package. A base package on which Jupyter projects rely." optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_core-5.7.1-py3-none-any.whl", hash = "sha256:c65c82126453a723a2804aa52409930434598fd9d35091d63dfb919d2b765bb7"}, - {file = "jupyter_core-5.7.1.tar.gz", hash = "sha256:de61a9d7fc71240f688b2fb5ab659fbb56979458dc66a71decd098e03c79e218"}, + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, ] [package.dependencies] @@ -1224,7 +1452,7 @@ traitlets = ">=5.3" [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] [[package]] name = "jupyterlab-pygments" @@ -1266,17 +1494,46 @@ test-functional = ["jupytext[test]"] test-integration = ["ipykernel", "jupyter-server (!=2.11)", "jupytext[test-functional]", "nbconvert"] test-ui = ["calysto-bash"] +[[package]] +name = "litellm" +version = "1.35.31" +description = "Library to easily interface with LLM API providers" +optional = false +python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" +files = [ + {file = "litellm-1.35.31-py3-none-any.whl", hash = "sha256:bf8b437201bbcde50f8508f8712dd7ac7f323a1cdc2628aba0d35a4c312a801d"}, + {file = "litellm-1.35.31.tar.gz", hash = "sha256:8691fddef3c14733dfef2f34c11daf47a317790a258c5315eabe7d1eafbfeb24"}, +] + +[package.dependencies] +aiohttp = "*" +click = "*" +importlib-metadata = ">=6.8.0" +jinja2 = ">=3.1.2,<4.0.0" +openai = ">=1.0.0" +python-dotenv = ">=0.2.0" +requests = ">=2.31.0,<3.0.0" +tiktoken = ">=0.4.0" +tokenizers = "*" + +[package.extras] +extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "resend (>=0.8.0,<0.9.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=42.0.5,<43.0.0)", "fastapi (>=0.109.1,<0.110.0)", "fastapi-sso (>=0.10.0,<0.11.0)", "gunicorn (>=21.2.0,<22.0.0)", "orjson (>=3.9.7,<4.0.0)", "python-multipart (>=0.0.9,<0.0.10)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.22.0,<0.23.0)"] + [[package]] name = "markdown" -version = "3.5.2" +version = "3.6" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, - {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + [package.extras] docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] @@ -1376,13 +1633,13 @@ files = [ [[package]] name = "matplotlib-inline" -version = "0.1.6" +version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] [package.dependencies] @@ -1429,6 +1686,22 @@ files = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] +[[package]] +name = "mistralai" +version = "0.1.8" +description = "" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "mistralai-0.1.8-py3-none-any.whl", hash = "sha256:3d74146d26ce0e93333614a5c591d407867c78a09855d4550c94a51c1775a1a6"}, + {file = "mistralai-0.1.8.tar.gz", hash = "sha256:c2e645f1cd26b35c728de9ef954927384d190f439a165a041bd935937fbc3f04"}, +] + +[package.dependencies] +httpx = ">=0.25.2,<0.26.0" +orjson = ">=3.9.10,<4.0.0" +pydantic = ">=2.5.2,<3.0.0" + [[package]] name = "mistune" version = "3.0.2" @@ -1442,58 +1715,76 @@ files = [ [[package]] name = "mkdocs" -version = "1.5.3" +version = "1.6.0" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, ] [package.dependencies] click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1" +markdown = ">=3.3.6" markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" -version = "0.5.0" +version = "1.0.1" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, - {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, ] [package.dependencies] Markdown = ">=3.3" +markupsafe = ">=2.0.1" mkdocs = ">=1.1" +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + [[package]] name = "mkdocs-jupyter" -version = "0.24.6" +version = "0.24.7" description = "Use Jupyter in mkdocs websites" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "mkdocs_jupyter-0.24.6-py3-none-any.whl", hash = "sha256:56fb7ad796f2414a4143d54a966b805caf315c32413e97f85591623fa87dceca"}, - {file = "mkdocs_jupyter-0.24.6.tar.gz", hash = "sha256:89fcbe8a9523864d5416de1a60711640b6bc2972279d2adf46ed2776c2d9ff7c"}, + {file = "mkdocs_jupyter-0.24.7-py3-none-any.whl", hash = "sha256:893d04bea1e007479a46e4e72852cd4d280c4d358ce4a0445250f3f80c639723"}, ] [package.dependencies] @@ -1506,13 +1797,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.11" +version = "9.5.20" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.11-py3-none-any.whl", hash = "sha256:788ee0f3e036dca2dc20298d65e480297d348a44c9d7b2ee05c5262983e66072"}, - {file = "mkdocs_material-9.5.11.tar.gz", hash = "sha256:7af7f8af0dea16175558f3fb9245d26c83a17199baa5f157755e63d7437bf971"}, + {file = "mkdocs_material-9.5.20-py3-none-any.whl", hash = "sha256:ad0094a7597bcb5d0cc3e8e543a10927c2581f7f647b9bb4861600f583180f9b"}, + {file = "mkdocs_material-9.5.20.tar.gz", hash = "sha256:986eef0250d22f70fb06ce0f4eac64cc92bd797a589ec3892ce31fad976fe3da"}, ] [package.dependencies] @@ -1521,7 +1812,7 @@ cairosvg = {version = ">=2.6,<3.0", optional = true, markers = "extra == \"imagi colorama = ">=0.4,<1.0" jinja2 = ">=3.0,<4.0" markdown = ">=3.2,<4.0" -mkdocs = ">=1.5.3,<1.6.0" +mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" paginate = ">=0.5,<1.0" pillow = {version = ">=10.2,<11.0", optional = true, markers = "extra == \"imaging\""} @@ -1614,12 +1905,14 @@ files = [ ] [package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.3" MarkupSafe = ">=1.1" mkdocs = ">=1.2" mkdocs-autorefs = ">=0.3.1" pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} [package.extras] crystal = ["mkdocstrings-crystal (>=0.3.4)"] @@ -1740,53 +2033,6 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] -[[package]] -name = "mypy" -version = "1.8.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1800,13 +2046,13 @@ files = [ [[package]] name = "nbclient" -version = "0.9.0" +version = "0.10.0" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = false python-versions = ">=3.8.0" files = [ - {file = "nbclient-0.9.0-py3-none-any.whl", hash = "sha256:a3a1ddfb34d4a9d17fc744d655962714a866639acd30130e9be84191cd97cd15"}, - {file = "nbclient-0.9.0.tar.gz", hash = "sha256:4b28c207877cf33ef3a9838cdc7a54c5ceff981194a82eac59d558f05487295e"}, + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, ] [package.dependencies] @@ -1818,23 +2064,24 @@ traitlets = ">=5.4" [package.extras] dev = ["pre-commit"] docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] [[package]] name = "nbconvert" -version = "7.16.1" +version = "7.16.4" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.16.1-py3-none-any.whl", hash = "sha256:3188727dffadfdc9c6a1c7250729063d7bc78b355ad7aa023138afa030d1cd07"}, - {file = "nbconvert-7.16.1.tar.gz", hash = "sha256:e79e6a074f49ba3ed29428ed86487bf51509d9aab613bd8522ac08f6d28fd7fd"}, + {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, + {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, ] [package.dependencies] beautifulsoup4 = "*" bleach = "!=5.0.0" defusedxml = "*" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} jinja2 = ">=3.0" jupyter-core = ">=4.7" jupyterlab-pygments = "*" @@ -1849,29 +2096,29 @@ tinycss2 = "*" traitlets = ">=5.1" [package.extras] -all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["nbconvert[qtpng]"] +qtpdf = ["pyqtwebengine (>=5.15)"] qtpng = ["pyqtwebengine (>=5.15)"] serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] webpdf = ["playwright"] [[package]] name = "nbformat" -version = "5.9.2" +version = "5.10.4" description = "The Jupyter Notebook format" optional = false python-versions = ">=3.8" files = [ - {file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"}, - {file = "nbformat-5.9.2.tar.gz", hash = "sha256:5f98b5ba1997dff175e77e0c17d5c10a96eaed2cbd1de3533d1fc35d5e111192"}, + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, ] [package.dependencies] -fastjsonschema = "*" +fastjsonschema = ">=2.15" jsonschema = ">=2.6" -jupyter-core = "*" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] @@ -1889,6 +2136,20 @@ files = [ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "numpy" version = "1.26.4" @@ -1936,13 +2197,13 @@ files = [ [[package]] name = "openai" -version = "1.12.0" +version = "1.23.6" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.12.0-py3-none-any.whl", hash = "sha256:a54002c814e05222e413664f651b5916714e4700d041d5cf5724d3ae1a3e3481"}, - {file = "openai-1.12.0.tar.gz", hash = "sha256:99c5d257d09ea6533d689d1cc77caa0ac679fa21efef8893d8b0832a86877f1b"}, + {file = "openai-1.23.6-py3-none-any.whl", hash = "sha256:f406c76ba279d16b9aca5a89cee0d968488e39f671f4dc6f0d690ac3c6f6fca1"}, + {file = "openai-1.23.6.tar.gz", hash = "sha256:612de2d54cf580920a1156273f84aada6b3dca26d048f62eb5364a4314d7f449"}, ] [package.dependencies] @@ -1957,15 +2218,75 @@ typing-extensions = ">=4.7,<5" [package.extras] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +[[package]] +name = "orjson" +version = "3.10.1" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8ec2fc456d53ea4a47768f622bb709be68acd455b0c6be57e91462259741c4f3"}, + {file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e900863691d327758be14e2a491931605bd0aded3a21beb6ce133889830b659"}, + {file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab6ecbd6fe57785ebc86ee49e183f37d45f91b46fc601380c67c5c5e9c0014a2"}, + {file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af7c68b01b876335cccfb4eee0beef2b5b6eae1945d46a09a7c24c9faac7a77"}, + {file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:915abfb2e528677b488a06eba173e9d7706a20fdfe9cdb15890b74ef9791b85e"}, + {file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3fd4a36eff9c63d25503b439531d21828da9def0059c4f472e3845a081aa0b"}, + {file = "orjson-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d229564e72cfc062e6481a91977a5165c5a0fdce11ddc19ced8471847a67c517"}, + {file = "orjson-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e00495b18304173ac843b5c5fbea7b6f7968564d0d49bef06bfaeca4b656f4e"}, + {file = "orjson-3.10.1-cp310-none-win32.whl", hash = "sha256:fd78ec55179545c108174ba19c1795ced548d6cac4d80d014163033c047ca4ea"}, + {file = "orjson-3.10.1-cp310-none-win_amd64.whl", hash = "sha256:50ca42b40d5a442a9e22eece8cf42ba3d7cd4cd0f2f20184b4d7682894f05eec"}, + {file = "orjson-3.10.1-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b345a3d6953628df2f42502297f6c1e1b475cfbf6268013c94c5ac80e8abc04c"}, + {file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caa7395ef51af4190d2c70a364e2f42138e0e5fcb4bc08bc9b76997659b27dab"}, + {file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b01d701decd75ae092e5f36f7b88a1e7a1d3bb7c9b9d7694de850fb155578d5a"}, + {file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5028981ba393f443d8fed9049211b979cadc9d0afecf162832f5a5b152c6297"}, + {file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31ff6a222ea362b87bf21ff619598a4dc1106aaafaea32b1c4876d692891ec27"}, + {file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e852a83d7803d3406135fb7a57cf0c1e4a3e73bac80ec621bd32f01c653849c5"}, + {file = "orjson-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2567bc928ed3c3fcd90998009e8835de7c7dc59aabcf764b8374d36044864f3b"}, + {file = "orjson-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4ce98cac60b7bb56457bdd2ed7f0d5d7f242d291fdc0ca566c83fa721b52e92d"}, + {file = "orjson-3.10.1-cp311-none-win32.whl", hash = "sha256:813905e111318acb356bb8029014c77b4c647f8b03f314e7b475bd9ce6d1a8ce"}, + {file = "orjson-3.10.1-cp311-none-win_amd64.whl", hash = "sha256:03a3ca0b3ed52bed1a869163a4284e8a7b0be6a0359d521e467cdef7e8e8a3ee"}, + {file = "orjson-3.10.1-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f02c06cee680b1b3a8727ec26c36f4b3c0c9e2b26339d64471034d16f74f4ef5"}, + {file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1aa2f127ac546e123283e437cc90b5ecce754a22306c7700b11035dad4ccf85"}, + {file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2cf29b4b74f585225196944dffdebd549ad2af6da9e80db7115984103fb18a96"}, + {file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1b130c20b116f413caf6059c651ad32215c28500dce9cd029a334a2d84aa66f"}, + {file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d31f9a709e6114492136e87c7c6da5e21dfedebefa03af85f3ad72656c493ae9"}, + {file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d1d169461726f271ab31633cf0e7e7353417e16fb69256a4f8ecb3246a78d6e"}, + {file = "orjson-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57c294d73825c6b7f30d11c9e5900cfec9a814893af7f14efbe06b8d0f25fba9"}, + {file = "orjson-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7f11dbacfa9265ec76b4019efffabaabba7a7ebf14078f6b4df9b51c3c9a8ea"}, + {file = "orjson-3.10.1-cp312-none-win32.whl", hash = "sha256:d89e5ed68593226c31c76ab4de3e0d35c760bfd3fbf0a74c4b2be1383a1bf123"}, + {file = "orjson-3.10.1-cp312-none-win_amd64.whl", hash = "sha256:aa76c4fe147fd162107ce1692c39f7189180cfd3a27cfbc2ab5643422812da8e"}, + {file = "orjson-3.10.1-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a2c6a85c92d0e494c1ae117befc93cf8e7bca2075f7fe52e32698da650b2c6d1"}, + {file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9813f43da955197d36a7365eb99bed42b83680801729ab2487fef305b9ced866"}, + {file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec917b768e2b34b7084cb6c68941f6de5812cc26c6f1a9fecb728e36a3deb9e8"}, + {file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5252146b3172d75c8a6d27ebca59c9ee066ffc5a277050ccec24821e68742fdf"}, + {file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:536429bb02791a199d976118b95014ad66f74c58b7644d21061c54ad284e00f4"}, + {file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dfed3c3e9b9199fb9c3355b9c7e4649b65f639e50ddf50efdf86b45c6de04b5"}, + {file = "orjson-3.10.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2b230ec35f188f003f5b543644ae486b2998f6afa74ee3a98fc8ed2e45960afc"}, + {file = "orjson-3.10.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:01234249ba19c6ab1eb0b8be89f13ea21218b2d72d496ef085cfd37e1bae9dd8"}, + {file = "orjson-3.10.1-cp38-none-win32.whl", hash = "sha256:8a884fbf81a3cc22d264ba780920d4885442144e6acaa1411921260416ac9a54"}, + {file = "orjson-3.10.1-cp38-none-win_amd64.whl", hash = "sha256:dab5f802d52b182163f307d2b1f727d30b1762e1923c64c9c56dd853f9671a49"}, + {file = "orjson-3.10.1-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a51fd55d4486bc5293b7a400f9acd55a2dc3b5fc8420d5ffe9b1d6bb1a056a5e"}, + {file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53521542a6db1411b3bfa1b24ddce18605a3abdc95a28a67b33f9145f26aa8f2"}, + {file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:27d610df96ac18ace4931411d489637d20ab3b8f63562b0531bba16011998db0"}, + {file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79244b1456e5846d44e9846534bd9e3206712936d026ea8e6a55a7374d2c0694"}, + {file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d751efaa8a49ae15cbebdda747a62a9ae521126e396fda8143858419f3b03610"}, + {file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ff69c620a4fff33267df70cfd21e0097c2a14216e72943bd5414943e376d77"}, + {file = "orjson-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebc58693464146506fde0c4eb1216ff6d4e40213e61f7d40e2f0dde9b2f21650"}, + {file = "orjson-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5be608c3972ed902e0143a5b8776d81ac1059436915d42defe5c6ae97b3137a4"}, + {file = "orjson-3.10.1-cp39-none-win32.whl", hash = "sha256:4ae10753e7511d359405aadcbf96556c86e9dbf3a948d26c2c9f9a150c52b091"}, + {file = "orjson-3.10.1-cp39-none-win_amd64.whl", hash = "sha256:fb5bc4caa2c192077fdb02dce4e5ef8639e7f20bec4e3a834346693907362932"}, + {file = "orjson-3.10.1.tar.gz", hash = "sha256:a883b28d73370df23ed995c466b4f6c708c1f7a9bdc400fe89165c96c7603204"}, +] + [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1980,47 +2301,44 @@ files = [ [[package]] name = "pandas" -version = "2.2.1" +version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, - {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, - {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, - {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, - {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, - {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, - {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, ] [package.dependencies] numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2064,18 +2382,18 @@ files = [ [[package]] name = "parso" -version = "0.8.3" +version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, ] [package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] [[package]] name = "pathspec" @@ -2102,81 +2420,93 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "phonenumbers" +version = "8.13.35" +description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." +optional = false +python-versions = "*" +files = [ + {file = "phonenumbers-8.13.35-py2.py3-none-any.whl", hash = "sha256:58286a8e617bd75f541e04313b28c36398be6d4443a778c85e9617a93c391310"}, + {file = "phonenumbers-8.13.35.tar.gz", hash = "sha256:64f061a967dcdae11e1c59f3688649e697b897110a33bb74d5a69c3e35321245"}, +] + [[package]] name = "pillow" -version = "10.2.0" +version = "10.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, - {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, - {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, - {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, - {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, - {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, - {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, - {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, - {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, - {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, - {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, - {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, - {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, - {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, - {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, - {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, - {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, - {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, ] [package.extras] @@ -2189,28 +2519,29 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -2286,29 +2617,29 @@ tests = ["pytest"] [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pydantic" -version = "2.6.2" +version = "2.7.0" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.2-py3-none-any.whl", hash = "sha256:37a5432e54b12fecaa1049c5195f3d860a10e01bdfd24f1840ef14bd0d3aeab3"}, - {file = "pydantic-2.6.2.tar.gz", hash = "sha256:a09be1c3d28f3abe37f8a78af58284b236a92ce520105ddc91a6d29ea1176ba7"}, + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" +pydantic-core = "2.18.1" typing-extensions = ">=4.6.1" [package.extras] @@ -2316,95 +2647,112 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.3" -description = "" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-extra-types" +version = "2.7.0" +description = "Extra Pydantic types." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_extra_types-2.7.0-py3-none-any.whl", hash = "sha256:ac01bbdaa4f85e4c4744a9792c5b0cfe61efa5028a0e670c3d8bfbf8b36c8543"}, + {file = "pydantic_extra_types-2.7.0.tar.gz", hash = "sha256:b9d9ddd755fa5960ec5a77cffcbd5d8796a0116e1dfc8f7c3a27fa0041693382"}, +] + +[package.dependencies] +pydantic = ">=2.5.2" + +[package.extras] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] + [[package]] name = "pygments" version = "2.17.2" @@ -2422,22 +2770,40 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.7" +version = "10.8.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, - {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, ] [package.dependencies] -markdown = ">=3.5" +markdown = ">=3.6" pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pyright" +version = "1.1.360" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.360-py3-none-any.whl", hash = "sha256:7637f75451ac968b7cf1f8c51cfefb6d60ac7d086eb845364bc8ac03a026efd7"}, + {file = "pyright-1.1.360.tar.gz", hash = "sha256:784ddcda9745e9f5610483d7b963e9aa8d4f50d7755a9dffb28ccbeb27adce32"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "pytest" version = "7.4.4" @@ -2462,13 +2828,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.21.1" +version = "0.21.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, - {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, ] [package.dependencies] @@ -2496,18 +2862,32 @@ ruff = ">=0.0.258" [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pytz" version = "2024.1" @@ -2554,7 +2934,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2562,15 +2941,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2587,7 +2959,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2595,7 +2966,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2617,104 +2987,99 @@ pyyaml = "*" [[package]] name = "pyzmq" -version = "25.1.2" +version = "26.0.2" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, - {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, - {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, - {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, - {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, - {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, - {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, - {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, - {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, - {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, - {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, - {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, - {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1a60a03b01e8c9c58932ec0cca15b1712d911c2800eb82d4281bc1ae5b6dad50"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:949067079e14ea1973bd740255e0840118c163d4bce8837f539d749f145cf5c3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e7edfa6cf96d036a403775c96afa25058d1bb940a79786a9a2fc94a783abe3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:903cc7a84a7d4326b43755c368780800e035aa3d711deae84a533fdffa8755b0"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cb2e41af165e5f327d06fbdd79a42a4e930267fade4e9f92d17f3ccce03f3a7"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:55353b8189adcfc4c125fc4ce59d477744118e9c0ec379dd0999c5fa120ac4f5"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f961423ff6236a752ced80057a20e623044df95924ed1009f844cde8b3a595f9"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ba77fe84fe4f5f3dc0ef681a6d366685c8ffe1c8439c1d7530997b05ac06a04b"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:52589f0a745ef61b9c75c872cf91f8c1f7c0668eb3dd99d7abd639d8c0fb9ca7"}, + {file = "pyzmq-26.0.2-cp310-cp310-win32.whl", hash = "sha256:b7b6d2a46c7afe2ad03ec8faf9967090c8ceae85c4d8934d17d7cae6f9062b64"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:86531e20de249d9204cc6d8b13d5a30537748c78820215161d8a3b9ea58ca111"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:f26a05029ecd2bd306b941ff8cb80f7620b7901421052bc429d238305b1cbf2f"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:70770e296a9cb03d955540c99360aab861cbb3cba29516abbd106a15dbd91268"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2740fd7161b39e178554ebf21aa5667a1c9ef0cd2cb74298fd4ef017dae7aec4"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3706c32dea077faa42b1c92d825b7f86c866f72532d342e0be5e64d14d858"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fa1416876194927f7723d6b7171b95e1115602967fc6bfccbc0d2d51d8ebae1"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef9a79a48794099c57dc2df00340b5d47c5caa1792f9ddb8c7a26b1280bd575"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1c60fcdfa3229aeee4291c5d60faed3a813b18bdadb86299c4bf49e8e51e8605"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e943c39c206b04df2eb5d71305761d7c3ca75fd49452115ea92db1b5b98dbdef"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8da0ed8a598693731c76659880a668f4748b59158f26ed283a93f7f04d47447e"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bf51970b11d67096bede97cdbad0f4333f7664f4708b9b2acb352bf4faa3140"}, + {file = "pyzmq-26.0.2-cp311-cp311-win32.whl", hash = "sha256:6f8e6bd5d066be605faa9fe5ec10aa1a46ad9f18fc8646f2b9aaefc8fb575742"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d03da3a0ae691b361edcb39530075461202f699ce05adbb15055a0e1c9bcaa4"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f84e33321b68ff00b60e9dbd1a483e31ab6022c577c8de525b8e771bd274ce68"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:44c33ebd1c62a01db7fbc24e18bdda569d6639217d13d5929e986a2b0f69070d"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ac04f904b4fce4afea9cdccbb78e24d468cb610a839d5a698853e14e2a3f9ecf"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2133de5ba9adc5f481884ccb699eac9ce789708292945c05746880f95b241c0"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7753c67c570d7fc80c2dc59b90ca1196f1224e0e2e29a548980c95fe0fe27fc1"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d4e51632e6b12e65e8d9d7612446ecda2eda637a868afa7bce16270194650dd"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d6c38806f6ecd0acf3104b8d7e76a206bcf56dadd6ce03720d2fa9d9157d5718"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:48f496bbe14686b51cec15406323ae6942851e14022efd7fc0e2ecd092c5982c"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e84a3161149c75bb7a7dc8646384186c34033e286a67fec1ad1bdedea165e7f4"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dabf796c67aa9f5a4fcc956d47f0d48b5c1ed288d628cf53aa1cf08e88654343"}, + {file = "pyzmq-26.0.2-cp312-cp312-win32.whl", hash = "sha256:3eee4c676af1b109f708d80ef0cf57ecb8aaa5900d1edaf90406aea7e0e20e37"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:26721fec65846b3e4450dad050d67d31b017f97e67f7e0647b5f98aa47f828cf"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:653955c6c233e90de128a1b8e882abc7216f41f44218056bd519969c8c413a15"}, + {file = "pyzmq-26.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:becd8d8fb068fbb5a52096efd83a2d8e54354383f691781f53a4c26aee944542"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7a15e5465e7083c12517209c9dd24722b25e9b63c49a563922922fc03554eb35"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8158ac8616941f874841f9fa0f6d2f1466178c2ff91ea08353fdc19de0d40c2"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c6a53e28c7066ea7db86fcc0b71d78d01b818bb11d4a4341ec35059885295"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bdbc7dab0b0e9c62c97b732899c4242e3282ba803bad668e03650b59b165466e"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e74b6d5ef57bb65bf1b4a37453d8d86d88550dde3fb0f23b1f1a24e60c70af5b"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ed4c6ee624ecbc77b18aeeb07bf0700d26571ab95b8f723f0d02e056b5bce438"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win32.whl", hash = "sha256:8a98b3cb0484b83c19d8fb5524c8a469cd9f10e743f5904ac285d92678ee761f"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:aa5f95d71b6eca9cec28aa0a2f8310ea53dea313b63db74932879ff860c1fb8d"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:5ff56c76ce77b9805378a7a73032c17cbdb1a5b84faa1df03c5d3e306e5616df"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bab697fc1574fee4b81da955678708567c43c813c84c91074e452bda5346c921"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0c0fed8aa9ba0488ee1cbdaa304deea92d52fab43d373297002cfcc69c0a20c5"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:606b922699fcec472ed814dda4dc3ff7c748254e0b26762a0ba21a726eb1c107"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f0fd82bad4d199fa993fbf0ac586a7ac5879addbe436a35a389df7e0eb4c91"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:166c5e41045939a52c01e6f374e493d9a6a45dfe677360d3e7026e38c42e8906"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d566e859e8b8d5bca08467c093061774924b3d78a5ba290e82735b2569edc84b"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:264ee0e72b72ca59279dc320deab5ae0fac0d97881aed1875ce4bde2e56ffde0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win32.whl", hash = "sha256:3152bbd3a4744cbdd83dfb210ed701838b8b0c9065cef14671d6d91df12197d0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:bf77601d75ca692c179154b7e5943c286a4aaffec02c491afe05e60493ce95f2"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:c770a7545b3deca2db185b59175e710a820dd4ed43619f4c02e90b0e227c6252"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d47175f0a380bfd051726bc5c0054036ae4a5d8caf922c62c8a172ccd95c1a2a"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bce298c1ce077837e110367c321285dc4246b531cde1abfc27e4a5bbe2bed4d"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c40b09b7e184d6e3e1be1c8af2cc320c0f9f610d8a5df3dd866e6e6e4e32b235"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d420d856bf728713874cefb911398efe69e1577835851dd297a308a78c14c249"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d792d3cab987058451e55c70c5926e93e2ceb68ca5a2334863bb903eb860c9cb"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83ec17729cf6d3464dab98a11e98294fcd50e6b17eaabd3d841515c23f6dbd3a"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47c17d5ebfa88ae90f08960c97b49917098665b8cd8be31f2c24e177bcf37a0f"}, + {file = "pyzmq-26.0.2-cp39-cp39-win32.whl", hash = "sha256:d509685d1cd1d018705a811c5f9d5bc237790936ead6d06f6558b77e16cc7235"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c7cc8cc009e8f6989a6d86c96f87dae5f5fb07d6c96916cdc7719d546152c7db"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:3ada31cb879cd7532f4a85b501f4255c747d4813ab76b35c49ed510ce4865b45"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0a6ceaddc830dd3ca86cb8451cf373d1f05215368e11834538c2902ed5205139"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a967681463aa7a99eb9a62bb18229b653b45c10ff0947b31cc0837a83dfb86f"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6472a73bc115bc40a2076609a90894775abe6faf19a78375675a2f889a613071"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d6aea92bcccfe5e5524d3c70a6f16ffdae548390ddad26f4207d55c55a40593"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e025f6351e49d48a5aa2f5a09293aa769b0ee7369c25bed551647234b7fa0c75"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:40bd7ebe4dbb37d27f0c56e2a844f360239343a99be422085e13e97da13f73f9"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1dd40d586ad6f53764104df6e01810fe1b4e88fd353774629a5e6fe253813f79"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2aca15e9ad8c8657b5b3d7ae3d1724dc8c1c1059c06b4b674c3aa36305f4930"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:450ec234736732eb0ebeffdb95a352450d4592f12c3e087e2a9183386d22c8bf"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f43be2bebbd09360a2f23af83b243dc25ffe7b583ea8c722e6df03e03a55f02f"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:867f55e54aff254940bcec5eec068e7c0ac1e6bf360ab91479394a8bf356b0e6"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4dbc033c5ad46f8c429bf238c25a889b8c1d86bfe23a74e1031a991cb3f0000"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6e8dd2961462e337e21092ec2da0c69d814dcb1b6e892955a37444a425e9cfb8"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35391e72df6c14a09b697c7b94384947c1dd326aca883ff98ff137acdf586c33"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1c3d3c92fa54eda94ab369ca5b8d35059987c326ba5e55326eb068862f64b1fc"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7aa61a9cc4f0523373e31fc9255bf4567185a099f85ca3598e64de484da3ab2"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee53a8191271f144cc20b12c19daa9f1546adc84a2f33839e3338039b55c373c"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac60a980f07fa988983f7bfe6404ef3f1e4303f5288a01713bc1266df6d18783"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88896b1b4817d7b2fe1ec7205c4bbe07bf5d92fb249bf2d226ddea8761996068"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:18dfffe23751edee917764ffa133d5d3fef28dfd1cf3adebef8c90bc854c74c4"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6926dd14cfe6967d3322640b6d5c3c3039db71716a5e43cca6e3b474e73e0b36"}, + {file = "pyzmq-26.0.2.tar.gz", hash = "sha256:f0f9bb370449158359bb72a3e12c658327670c0ffe6fbcd1af083152b64f9df0"}, ] [package.dependencies] @@ -2722,17 +3087,17 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "redis" -version = "5.0.1" +version = "5.0.4" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, - {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, + {file = "redis-5.0.4-py3-none-any.whl", hash = "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91"}, + {file = "redis-5.0.4.tar.gz", hash = "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61"}, ] [package.dependencies] -async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -2740,13 +3105,13 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "referencing" -version = "0.33.0" +version = "0.35.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, - {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, + {file = "referencing-0.35.0-py3-none-any.whl", hash = "sha256:8080727b30e364e5783152903672df9b6b091c926a146a759080b62ca3126cd6"}, + {file = "referencing-0.35.0.tar.gz", hash = "sha256:191e936b0c696d0af17ad7430a3dc68e88bc11be6514f4757dc890f04ab05889"}, ] [package.dependencies] @@ -2755,104 +3120,90 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2023.12.25" +version = "2024.4.28" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, - {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, - {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, - {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, - {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, - {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, - {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, - {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, - {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, - {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, - {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, - {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, - {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, - {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, - {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, + {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61"}, + {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f"}, + {file = "regex-2024.4.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c"}, + {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10"}, + {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3"}, + {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db"}, + {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e"}, + {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8"}, + {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea"}, + {file = "regex-2024.4.28-cp310-cp310-win32.whl", hash = "sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d"}, + {file = "regex-2024.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1"}, + {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796"}, + {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb"}, + {file = "regex-2024.4.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3"}, + {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6"}, + {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a"}, + {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec"}, + {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb"}, + {file = "regex-2024.4.28-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636"}, + {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b"}, + {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a"}, + {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257"}, + {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf"}, + {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f"}, + {file = "regex-2024.4.28-cp311-cp311-win32.whl", hash = "sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630"}, + {file = "regex-2024.4.28-cp311-cp311-win_amd64.whl", hash = "sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90"}, + {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5"}, + {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae"}, + {file = "regex-2024.4.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0"}, + {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013"}, + {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662"}, + {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc"}, + {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c"}, + {file = "regex-2024.4.28-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833"}, + {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd"}, + {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f"}, + {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0"}, + {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d"}, + {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6"}, + {file = "regex-2024.4.28-cp312-cp312-win32.whl", hash = "sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17"}, + {file = "regex-2024.4.28-cp312-cp312-win_amd64.whl", hash = "sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82"}, + {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6"}, + {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666"}, + {file = "regex-2024.4.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94"}, + {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925"}, + {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4"}, + {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e"}, + {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02"}, + {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e"}, + {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150"}, + {file = "regex-2024.4.28-cp38-cp38-win32.whl", hash = "sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58"}, + {file = "regex-2024.4.28-cp38-cp38-win_amd64.whl", hash = "sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c"}, + {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b"}, + {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae"}, + {file = "regex-2024.4.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2"}, + {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26"}, + {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d"}, + {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f"}, + {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc"}, + {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a"}, + {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1"}, + {file = "regex-2024.4.28-cp39-cp39-win32.whl", hash = "sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc"}, + {file = "regex-2024.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962"}, + {file = "regex-2024.4.28.tar.gz", hash = "sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4"}, ] [[package]] @@ -2878,13 +3229,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -3004,28 +3355,55 @@ files = [ [[package]] name = "ruff" -version = "0.2.2" +version = "0.4.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, +] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] @@ -3104,6 +3482,7 @@ files = [ [package.dependencies] anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] @@ -3136,15 +3515,67 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "tiktoken" +version = "0.6.0" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tiktoken-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:277de84ccd8fa12730a6b4067456e5cf72fef6300bea61d506c09e45658d41ac"}, + {file = "tiktoken-0.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c44433f658064463650d61387623735641dcc4b6c999ca30bc0f8ba3fccaf5c"}, + {file = "tiktoken-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb9a2a866ae6eef1995ab656744287a5ac95acc7e0491c33fad54d053288ad3"}, + {file = "tiktoken-0.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c62c05b3109fefca26fedb2820452a050074ad8e5ad9803f4652977778177d9f"}, + {file = "tiktoken-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ef917fad0bccda07bfbad835525bbed5f3ab97a8a3e66526e48cdc3e7beacf7"}, + {file = "tiktoken-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e095131ab6092d0769a2fda85aa260c7c383072daec599ba9d8b149d2a3f4d8b"}, + {file = "tiktoken-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:05b344c61779f815038292a19a0c6eb7098b63c8f865ff205abb9ea1b656030e"}, + {file = "tiktoken-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cefb9870fb55dca9e450e54dbf61f904aab9180ff6fe568b61f4db9564e78871"}, + {file = "tiktoken-0.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:702950d33d8cabc039845674107d2e6dcabbbb0990ef350f640661368df481bb"}, + {file = "tiktoken-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d49d076058f23254f2aff9af603863c5c5f9ab095bc896bceed04f8f0b013a"}, + {file = "tiktoken-0.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:430bc4e650a2d23a789dc2cdca3b9e5e7eb3cd3935168d97d43518cbb1f9a911"}, + {file = "tiktoken-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:293cb8669757301a3019a12d6770bd55bec38a4d3ee9978ddbe599d68976aca7"}, + {file = "tiktoken-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bd1a288b7903aadc054b0e16ea78e3171f70b670e7372432298c686ebf9dd47"}, + {file = "tiktoken-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac76e000183e3b749634968a45c7169b351e99936ef46f0d2353cd0d46c3118d"}, + {file = "tiktoken-0.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17cc8a4a3245ab7d935c83a2db6bb71619099d7284b884f4b2aea4c74f2f83e3"}, + {file = "tiktoken-0.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:284aebcccffe1bba0d6571651317df6a5b376ff6cfed5aeb800c55df44c78177"}, + {file = "tiktoken-0.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c1a3a5d33846f8cd9dd3b7897c1d45722f48625a587f8e6f3d3e85080559be8"}, + {file = "tiktoken-0.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6318b2bb2337f38ee954fd5efa82632c6e5ced1d52a671370fa4b2eff1355e91"}, + {file = "tiktoken-0.6.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f5f0f2ed67ba16373f9a6013b68da298096b27cd4e1cf276d2d3868b5c7efd1"}, + {file = "tiktoken-0.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:75af4c0b16609c2ad02581f3cdcd1fb698c7565091370bf6c0cf8624ffaba6dc"}, + {file = "tiktoken-0.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:45577faf9a9d383b8fd683e313cf6df88b6076c034f0a16da243bb1c139340c3"}, + {file = "tiktoken-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c1492ab90c21ca4d11cef3a236ee31a3e279bb21b3fc5b0e2210588c4209e68"}, + {file = "tiktoken-0.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e2b380c5b7751272015400b26144a2bab4066ebb8daae9c3cd2a92c3b508fe5a"}, + {file = "tiktoken-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f497598b9f58c99cbc0eb764b4a92272c14d5203fc713dd650b896a03a50ad"}, + {file = "tiktoken-0.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e65e8bd6f3f279d80f1e1fbd5f588f036b9a5fa27690b7f0cc07021f1dfa0839"}, + {file = "tiktoken-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5f1495450a54e564d236769d25bfefbf77727e232d7a8a378f97acddee08c1ae"}, + {file = "tiktoken-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6c4e4857d99f6fb4670e928250835b21b68c59250520a1941618b5b4194e20c3"}, + {file = "tiktoken-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:168d718f07a39b013032741867e789971346df8e89983fe3c0ef3fbd5a0b1cb9"}, + {file = "tiktoken-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47fdcfe11bd55376785a6aea8ad1db967db7f66ea81aed5c43fad497521819a4"}, + {file = "tiktoken-0.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb7d2ccbf1a7784810aff6b80b4012fb42c6fc37eaa68cb3b553801a5cc2d1fc"}, + {file = "tiktoken-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ccb7a111ee76af5d876a729a347f8747d5ad548e1487eeea90eaf58894b3138"}, + {file = "tiktoken-0.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2048e1086b48e3c8c6e2ceeac866561374cd57a84622fa49a6b245ffecb7744"}, + {file = "tiktoken-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07f229a5eb250b6403a61200199cecf0aac4aa23c3ecc1c11c1ca002cbb8f159"}, + {file = "tiktoken-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:432aa3be8436177b0db5a2b3e7cc28fd6c693f783b2f8722539ba16a867d0c6a"}, + {file = "tiktoken-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:8bfe8a19c8b5c40d121ee7938cd9c6a278e5b97dc035fd61714b4f0399d2f7a1"}, + {file = "tiktoken-0.6.0.tar.gz", hash = "sha256:ace62a4ede83c75b0374a2ddfa4b76903cf483e9cb06247f566be3bf14e6beed"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tinycss2" -version = "1.2.1" +version = "1.3.0" description = "A tiny CSS parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, + {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, + {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, ] [package.dependencies] @@ -3152,7 +3583,124 @@ webencodings = ">=0.4" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] +test = ["pytest", "ruff"] + +[[package]] +name = "tokenizers" +version = "0.19.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, + {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, + {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, + {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, + {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, + {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, + {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, + {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, + {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, + {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, + {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, + {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, + {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, + {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, + {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, + {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, + {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, + {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, + {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, + {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, + {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, + {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, + {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, + {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, + {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, + {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, + {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, + {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, + {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, + {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, + {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, + {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, + {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, + {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, + {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, + {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, + {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, +] + +[package.dependencies] +huggingface-hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] [[package]] name = "toml" @@ -3218,49 +3766,59 @@ telegram = ["requests"] [[package]] name = "traitlets" -version = "5.14.1" +version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, - {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typer" -version = "0.9.0" +version = "0.12.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, - {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, ] [package.dependencies] -click = ">=7.1.1,<9.0.0" +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +[[package]] +name = "types-requests" +version = "2.31.0.20240406" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, +] + +[package.dependencies] +urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] @@ -3354,6 +3912,17 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + [[package]] name = "yarl" version = "1.9.4" @@ -3457,7 +4026,30 @@ files = [ idna = ">=2.0" multidict = ">=4.0" +[[package]] +name = "zipp" +version = "3.18.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[extras] +anthropic = ["anthropic", "xmltodict"] +cohere = ["cohere"] +groq = ["groq"] +litellm = ["litellm"] +mistralai = ["mistralai"] +test-docs = ["anthropic", "cohere", "diskcache", "fastapi", "groq", "litellm", "mistralai", "pandas", "pydantic_extra_types", "redis", "tabulate"] + [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "e1fc2000159090bdfca18574e8169c5dd2e9b82228e233837d5eff5739f34a54" +python-versions = "^3.9" +content-hash = "7cbfa1cf980234b7f9cfa37ecd4fd43c6aa018608da49e8f9b9b09c48f1c18ad" diff --git a/pyproject.toml b/pyproject.toml index b7f69665c..64fd29f9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "instructor" -version = "0.6.2" +version = "1.2.6" description = "structured outputs for llm" authors = ["Jason Liu "] license = "MIT" @@ -9,14 +9,37 @@ packages = [{include = "instructor"}] repository = "https://github.com/jxnl/instructor" [tool.poetry.dependencies] -python = "^3.10" +python = "^3.9" openai = "^1.1.0" -pydantic = "^2.0.2" -docstring-parser = "^0.15" -typer = "^0.9.0" +pydantic = "^2.7.0" +docstring-parser = "^0.16" +typer = ">=0.9.0,<1.0.0" rich = "^13.7.0" aiohttp = "^3.9.1" tenacity = "^8.2.3" +pydantic-core = "^2.18.0" + +# dependency versions for extras +fastapi = { version = "^0.109.2", optional = true } +redis = { version = "^5.0.1", optional = true } +diskcache = { version = "^5.6.3", optional = true } +pandas = { version = "^2.2.0", optional = true } +tabulate = { version = "^0.9.0", optional = true } +pydantic_extra_types = { version = "^2.6.0", optional = true } +litellm = { version = "^1.35.31", optional = true } +anthropic = { version = "^0.23.1", optional = true } +xmltodict = { version = "^0.13.0", optional = true } +groq = { version = "^0.4.2", optional = true } +cohere = { version = "^5.1.8", optional = true } +mistralai = { version = "^0.1.8", optional = true } + +[tool.poetry.extras] +anthropic = ["anthropic", "xmltodict"] +groq = ["groq"] +cohere = ["cohere"] +test-docs = ["fastapi", "redis", "diskcache", "pandas", "tabulate", "pydantic_extra_types", "litellm", "anthropic", "groq", "cohere", "mistralai"] +mistralai = ["mistralai"] +litellm = ["litellm"] [tool.poetry.scripts] instructor = "instructor.cli.cli:app" @@ -25,7 +48,7 @@ instructor = "instructor.cli.cli:app" pytest = "^7.4.0" pytest-asyncio = "^0.21.1" coverage = "^7.3.2" -mypy = "^1.7.1" +pyright = "^1.1.360" [tool.poetry.group.docs.dependencies] mkdocs = "^1.4.3" @@ -38,14 +61,42 @@ mkdocs-rss-plugin = "^1.12.0" mkdocs-minify-plugin = "^0.8.0" mkdocs-redirects = "^1.2.1" +[tool.poetry.group.anthropic.dependencies] +anthropic = "^0.23.1" + [tool.poetry.group.test-docs.dependencies] fastapi = "^0.109.2" redis = "^5.0.1" diskcache = "^5.6.3" pandas = "^2.2.0" tabulate = "^0.9.0" +pydantic_extra_types = "^2.6.0" +litellm = "^1.35.31" +anthropic = "^0.23.1" +xmltodict = "^0.13.0" +groq = "^0.4.2" +phonenumbers = "^8.13.33" +cohere = "^5.1.8" +mistralai = "^0.1.8" +[tool.poetry.group.litellm.dependencies] +litellm = "^1.35.31" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.pyright] +include = ["instructor"] +exclude = [ + "**/node_modules", + "**/__pycache__", + "src/experimental", + "src/typestubs", + "**/tests/**", +] +pythonVersion = "3.9" +typeCheckingMode = "strict" +# Allow "redundant" runtime type-checking. +reportUnnecessaryIsInstance = "none" +reportUnnecessaryTypeIgnoreComment = "error" diff --git a/requirements-doc.txt b/requirements-doc.txt index 4c1944a65..588a94fda 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -4,6 +4,7 @@ cairosvg mkdocstrings mkdocstrings-python mkdocs-jupyter +mkdocs-redirects openai pydantic pytest>=7.4.0,<8.0.0 @@ -13,3 +14,4 @@ yarl==1.8.1 frozenlist==1.3.1 mkdocs-minify-plugin mkdocs-rss-plugin +pytest-examples \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4f8f563c5..99efad98a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ rich aiohttp ruff==0.1.7 pre-commit==3.5.0 -mypy==1.7.1 +pyright==1.1.360 typer +cohere \ No newline at end of file diff --git a/tests/dsl/test_partial.py b/tests/dsl/test_partial.py new file mode 100644 index 000000000..b10374b8d --- /dev/null +++ b/tests/dsl/test_partial.py @@ -0,0 +1,68 @@ +# type: ignore[all] +from pydantic import BaseModel + +from instructor.dsl.partial import Partial + + +class SampleNestedPartial(BaseModel): + b: int + + +class SamplePartial(BaseModel): + a: int + b: SampleNestedPartial + + +def test_partial(): + partial = Partial[SamplePartial] + assert partial.model_json_schema() == { + "$defs": { + "PartialSampleNestedPartial": { + "properties": {"b": {"title": "B", "type": "integer"}}, + "required": ["b"], + "title": "PartialSampleNestedPartial", + "type": "object", + } + }, + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"$ref": "#/$defs/PartialSampleNestedPartial"}, + }, + "required": ["a", "b"], + "title": "PartialSamplePartial", + "type": "object", + }, "Wrapped model JSON schema has changed" + assert partial.get_partial_model().model_json_schema() == { + "$defs": { + "PartialSampleNestedPartial": { + "properties": { + "b": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": None, + "title": "B", + } + }, + "title": "PartialSampleNestedPartial", + "type": "object", + } + }, + "properties": { + "a": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": None, + "title": "A", + }, + "b": { + "anyOf": [ + {"$ref": "#/$defs/PartialSampleNestedPartial"}, + {"type": "null"}, + ], + "default": {}, + }, + }, + "title": "PartialSamplePartial", + "type": "object", + }, "Partial model JSON schema has changed" + + for model in partial.model_from_chunks(['{"b": {"b": 1}}']): + assert model.model_dump() == {"a": None, "b": {"b": 1}} diff --git a/tests/openai/__init__.py b/tests/llm/test_anthropic/__init__.py similarity index 100% rename from tests/openai/__init__.py rename to tests/llm/test_anthropic/__init__.py diff --git a/tests/llm/test_anthropic/evals/test_simple.py b/tests/llm/test_anthropic/evals/test_simple.py new file mode 100644 index 000000000..2ebdadfd9 --- /dev/null +++ b/tests/llm/test_anthropic/evals/test_simple.py @@ -0,0 +1,251 @@ +from enum import Enum +from typing import Literal + +import anthropic +import pytest +from pydantic import BaseModel, field_validator + +import instructor +from instructor.retry import InstructorRetryException + +client = instructor.from_anthropic( + anthropic.Anthropic(), mode=instructor.Mode.ANTHROPIC_TOOLS +) + + +def test_simple(): + class User(BaseModel): + name: str + age: int + + @field_validator("name") + def name_is_uppercase(cls, v: str): + assert v.isupper(), "Name must be uppercase, please fix" + return v + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=2, + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old.", + } + ], + response_model=User, + ) # type: ignore + + assert isinstance(resp, User) + assert resp.name == "JOHN" # due to validation + assert resp.age == 18 + + +def test_nested_type(): + class Address(BaseModel): + house_number: int + street_name: str + + class User(BaseModel): + name: str + age: int + address: Address + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old and lives at 123 First Avenue.", + } + ], + response_model=User, + ) # type: ignore + + assert isinstance(resp, User) + assert resp.name == "John" + assert resp.age == 18 + + assert isinstance(resp.address, Address) + assert resp.address.house_number == 123 + assert resp.address.street_name == "First Avenue" + + +def test_list_str(): + class User(BaseModel): + name: str + age: int + family: list[str] + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name, age, and family members.", + } + ], + response_model=User, + ) + + assert isinstance(resp, User) + assert isinstance(resp.family, list) + for member in resp.family: + assert isinstance(member, str) + + +@pytest.mark.skip("Just use Literal!") +def test_enum(): + class Role(str, Enum): + ADMIN = "admin" + USER = "user" + + class User(BaseModel): + name: str + role: Role + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=1, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name and role of admin.", + } + ], + response_model=User, + ) + + assert isinstance(resp, User) + assert resp.role == Role.ADMIN + + +def test_literal(): + class User(BaseModel): + name: str + role: Literal["admin", "user"] + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=2, + messages=[ + { + "role": "user", + "content": "Create a admin user for a model with a name and role.", + } + ], + response_model=User, + ) # type: ignore + + assert isinstance(resp, User) + assert resp.role == "admin" + + +def test_nested_list(): + class Properties(BaseModel): + key: str + value: str + + class User(BaseModel): + name: str + age: int + properties: list[Properties] + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + { + "role": "user", + "content": "Create a user for a model with a name, age, and properties.", + } + ], + response_model=User, + ) + + assert isinstance(resp, User) + for property in resp.properties: + assert isinstance(property, Properties) + + +def test_system_messages_allcaps(): + class User(BaseModel): + name: str + age: int + + resp = client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=0, + messages=[ + {"role": "system", "content": "EVERYTHING MUST BE IN ALL CAPS"}, + { + "role": "user", + "content": "Create a user for a model with a name and age.", + }, + ], + response_model=User, + ) + + assert isinstance(resp, User) + assert resp.name.isupper() + + +def test_retry_error(): + class User(BaseModel): + name: str + + @field_validator("name") + def validate_name(cls, _): + raise ValueError("Never succeed") + + try: + client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=2, + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old", + }, + ], + response_model=User, + ) + except InstructorRetryException as e: + assert e.total_usage.input_tokens > 0 and e.total_usage.output_tokens > 0 + + +@pytest.mark.asyncio +async def test_async_retry_error(): + client = instructor.from_anthropic(anthropic.AsyncAnthropic()) + + class User(BaseModel): + name: str + + @field_validator("name") + def validate_name(cls, _): + raise ValueError("Never succeed") + + try: + await client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1024, + max_retries=2, + messages=[ + { + "role": "user", + "content": "Extract John is 18 years old", + }, + ], + response_model=User, + ) + except InstructorRetryException as e: + assert e.total_usage.input_tokens > 0 and e.total_usage.output_tokens > 0 diff --git a/tests/llm/test_new_client.py b/tests/llm/test_new_client.py new file mode 100644 index 000000000..5acf4e6a7 --- /dev/null +++ b/tests/llm/test_new_client.py @@ -0,0 +1,375 @@ +import cohere +import os +import openai +import instructor +import anthropic +import pytest +from pydantic import BaseModel, Field + + +class User(BaseModel): + name: str + age: int + + +def test_client_create(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + user = client.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +def test_client_messages_create(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + user = client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +def test_client_chat_completions_create_with_response(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + user, completion = client.chat.completions.create_with_completion( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + from openai.types.chat import ChatCompletion + + assert isinstance(completion, ChatCompletion) + + +def test_client_chat_completions_create(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + user = client.chat.completions.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +def test_client_chat_completions_create_partial(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + for user in client.chat.completions.create_partial( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ): + assert isinstance(user, User) + + +def test_client_chat_completions_create_iterable(): + client = instructor.from_openai(openai.OpenAI(), model="gpt-3.5-turbo") + + users = [ + user + for user in client.chat.completions.create_iterable( + response_model=User, + messages=[{"role": "user", "content": "Alice is 25, Bob is 30"}], + temperature=0, + ) + ] + assert len(users) == 2 + + +@pytest.mark.asyncio +async def test_async_client_chat_completions_create(): + client = openai.AsyncOpenAI() + instructor_client = instructor.from_openai(client, model="gpt-3.5-turbo") + + user = await instructor_client.chat.completions.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.asyncio +async def test_async_client_chat_completions_create_partial(): + client = openai.AsyncOpenAI() + instructor_client = instructor.from_openai(client, model="gpt-3.5-turbo") + + async for user in instructor_client.chat.completions.create_partial( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ): + assert isinstance(user, User) + + +@pytest.mark.asyncio +async def test_async_client_chat_completions_create_iterable(): + client = openai.AsyncOpenAI() + instructor_client = instructor.from_openai(client, model="gpt-3.5-turbo") + + async for user in instructor_client.chat.completions.create_iterable( + response_model=User, + messages=[{"role": "user", "content": "Alice is 25, Bob is 30"}], + temperature=0, + ): + assert isinstance(user, User) + + +@pytest.mark.asyncio +async def test_async_client_chat_completions_create_with_response(): + client = openai.AsyncOpenAI() + instructor_client = instructor.from_openai(client, model="gpt-3.5-turbo") + + user, response = await instructor_client.chat.completions.create_with_completion( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + from openai.types.chat import ChatCompletion + + assert user.name == "Jason" + assert user.age == 10 + assert isinstance(response, ChatCompletion) + + +def test_client_from_anthropic_with_response(): + client = instructor.from_anthropic( + anthropic.Anthropic(), + max_tokens=1000, + model="claude-3-haiku-20240307", + ) + + user, response = client.messages.create_with_completion( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + assert isinstance(response, anthropic.types.Message) + + +def test_client_anthropic_response(): + client = anthropic.Anthropic() + instructor_client = instructor.from_anthropic( + client, + max_tokens=1000, + model="claude-3-haiku-20240307", + ) + + user = instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.skip(reason="Skip for now") +def test_client_anthropic_bedrock_response(): + client = anthropic.AnthropicBedrock( + aws_access_key=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + aws_session_token=os.getenv("AWS_SESSION_TOKEN"), + aws_region=os.getenv("AWS_REGION_NAME"), + ) + + instructor_client = instructor.from_anthropic( + client, + max_tokens=1000, + model="anthropic.claude-3-haiku-20240307-v1:0", + ) + + user = instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.asyncio +async def test_async_client_anthropic_response(): + client = anthropic.AsyncAnthropic() + instructor_client = instructor.from_anthropic( + client, + max_tokens=1000, + model="claude-3-haiku-20240307", + ) + + user = await instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.skip(reason="Skip for now") +@pytest.mark.asyncio +async def test_async_client_anthropic_bedrock_response(): + client = anthropic.AsyncAnthropicBedrock( + aws_access_key=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + aws_session_token=os.getenv("AWS_SESSION_TOKEN"), + aws_region=os.getenv("AWS_REGION_NAME"), + ) + + instructor_client = instructor.from_anthropic( + client, + max_tokens=1000, + model="anthropic.claude-3-haiku-20240307-v1:0", + ) + + user = await instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.skip(reason="Skipping if Cohere API is not available") +def test_client_cohere_response(): + client = cohere.Client() + instructor_client = instructor.from_cohere( + client, + max_tokens=1000, + model="command-r-plus", + ) + + user = instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.skip(reason="Skipping if Cohere API is not available") +def test_client_cohere_response_with_nested_classes(): + client = cohere.Client() + instructor_client = instructor.from_cohere( + client, + max_tokens=1000, + model="command-r-plus", + ) + + class Person(BaseModel): + name: str = Field(description="name of the person") + country_of_origin: str = Field(description="country of origin of the person") + + class Group(BaseModel): + group_name: str = Field(description="name of the group") + members: list[Person] = Field(description="list of members in the group") + + task = """\ + Given the following text, create a Group object for 'The Beatles' band + + Text: + The Beatles were an English rock band formed in Liverpool in 1960. With a line-up comprising John Lennon, Paul McCartney, George Harrison and Ringo Starr, they are regarded as the most influential band of all time. The group were integral to the development of 1960s counterculture and popular music's recognition as an art form. + """ + group = instructor_client.messages.create( + response_model=Group, + messages=[{"role": "user", "content": task}], + temperature=0, + ) + assert group.group_name == "The Beatles" + assert len(group.members) == 4 + assert group.members[0].name == "John Lennon" + assert group.members[1].name == "Paul McCartney" + assert group.members[2].name == "George Harrison" + assert group.members[3].name == "Ringo Starr" + + +@pytest.mark.skip(reason="Skipping if Cohere API is not available") +@pytest.mark.asyncio +async def test_client_cohere_async(): + client = cohere.AsyncClient() + instructor_client = instructor.from_cohere( + client, + max_tokens=1000, + model="command-r-plus", + ) + + class Person(BaseModel): + name: str = Field(description="name of the person") + country_of_origin: str = Field(description="country of origin of the person") + + class Group(BaseModel): + group_name: str = Field(description="name of the group") + members: list[Person] = Field(description="list of members in the group") + + task = """\ + Given the following text, create a Group object for 'The Beatles' band + + Text: + The Beatles were an English rock band formed in Liverpool in 1960. With a line-up comprising John Lennon, Paul McCartney, George Harrison and Ringo Starr, they are regarded as the most influential band of all time. The group were integral to the development of 1960s counterculture and popular music's recognition as an art form. + """ + group = await instructor_client.messages.create( + response_model=Group, + messages=[{"role": "user", "content": task}], + temperature=0, + ) + assert group.group_name == "The Beatles" + assert len(group.members) == 4 + assert group.members[0].name == "John Lennon" + assert group.members[1].name == "Paul McCartney" + assert group.members[2].name == "George Harrison" + assert group.members[3].name == "Ringo Starr" + + +@pytest.mark.skip(reason="Skip for now") +def test_client_from_mistral_with_response(): + import mistralai.client as mistralaicli + + client = instructor.from_mistral( + mistralaicli.MistralClient(), + max_tokens=1000, + model="mistral-large-latest", + ) + + user, response = client.messages.create_with_completion( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 + + +@pytest.mark.skip(reason="Skip for now") +def test_client_mistral_response(): + import mistralai.client as mistralaicli + + client = mistralaicli.MistralClient() + instructor_client = instructor.from_mistral( + client, max_tokens=1000, model="mistral-large-latest" + ) + + user = instructor_client.messages.create( + response_model=User, + messages=[{"role": "user", "content": "Jason is 10"}], + temperature=0, + ) + assert user.name == "Jason" + assert user.age == 10 diff --git a/tests/llm/test_openai/__init__.py b/tests/llm/test_openai/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openai/conftest.py b/tests/llm/test_openai/conftest.py similarity index 100% rename from tests/openai/conftest.py rename to tests/llm/test_openai/conftest.py diff --git a/tests/openai/docs/test_docs.py b/tests/llm/test_openai/docs/test_docs.py similarity index 100% rename from tests/openai/docs/test_docs.py rename to tests/llm/test_openai/docs/test_docs.py diff --git a/tests/openai/docs/test_hub.py b/tests/llm/test_openai/docs/test_hub.py similarity index 61% rename from tests/openai/docs/test_hub.py rename to tests/llm/test_openai/docs/test_hub.py index 0932f032c..afe9303af 100644 --- a/tests/openai/docs/test_hub.py +++ b/tests/llm/test_openai/docs/test_hub.py @@ -4,10 +4,16 @@ @pytest.mark.parametrize("example", find_examples("docs/hub"), ids=str) def test_format_blog(example: CodeExample, eval_example: EvalExample): - if "ollama" in example.source: - return - - if "llama_cpp" in example.source: + excluded_sources = [ + "mistral", + "ollama", + "llama_cpp", + "groq", + "youtube", + "contact", + "langsmith", + ] # sources that are not supported in testing + if any(source in example.source for source in excluded_sources): return if eval_example.update_examples: diff --git a/tests/openai/docs/test_mkdocs.py b/tests/llm/test_openai/docs/test_mkdocs.py similarity index 100% rename from tests/openai/docs/test_mkdocs.py rename to tests/llm/test_openai/docs/test_mkdocs.py diff --git a/tests/llm/test_openai/evals/__init__.py b/tests/llm/test_openai/evals/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openai/evals/readme.md b/tests/llm/test_openai/evals/readme.md similarity index 100% rename from tests/openai/evals/readme.md rename to tests/llm/test_openai/evals/readme.md diff --git a/tests/openai/evals/test_classification_enums.py b/tests/llm/test_openai/evals/test_classification_enums.py similarity index 92% rename from tests/openai/evals/test_classification_enums.py rename to tests/llm/test_openai/evals/test_classification_enums.py index e9d759781..e42ec3305 100644 --- a/tests/openai/evals/test_classification_enums.py +++ b/tests/llm/test_openai/evals/test_classification_enums.py @@ -1,6 +1,5 @@ import enum from itertools import product -from typing import List import pytest import instructor @@ -8,7 +7,7 @@ from pydantic import BaseModel from instructor.function_calls import Mode -from tests.openai.util import models, modes +from ..util import models, modes class Labels(str, enum.Enum): @@ -38,7 +37,7 @@ class SinglePrediction(BaseModel): @pytest.mark.parametrize("model, data, mode", product(models, data, modes)) def test_classification(model, data, mode, client): - client = instructor.patch(client, mode=mode) + client = instructor.from_openai(client, mode=mode) if mode == instructor.Mode.JSON and model in {"gpt-3.5-turbo", "gpt-4"}: pytest.skip( @@ -68,7 +67,7 @@ class MultiLabels(str, enum.Enum): # Adjust the prediction model to accommodate a list of labels class MultiClassPrediction(BaseModel): - predicted_labels: List[MultiLabels] + predicted_labels: list[MultiLabels] data = [ @@ -89,7 +88,7 @@ class MultiClassPrediction(BaseModel): @pytest.mark.parametrize("model, data, mode", product(models, data, modes)) def test_multi_classify(model, data, mode, client): - client = instructor.patch(client, mode=mode) + client = instructor.from_openai(client, mode=mode) if (mode, model) in { (Mode.JSON, "gpt-3.5-turbo"), diff --git a/tests/openai/evals/test_classification_literals.py b/tests/llm/test_openai/evals/test_classification_literals.py similarity index 90% rename from tests/openai/evals/test_classification_literals.py rename to tests/llm/test_openai/evals/test_classification_literals.py index 5c00e7579..76252ab85 100644 --- a/tests/openai/evals/test_classification_literals.py +++ b/tests/llm/test_openai/evals/test_classification_literals.py @@ -1,5 +1,5 @@ from itertools import product -from typing import List, Literal +from typing import Literal import pytest import instructor @@ -7,7 +7,7 @@ from pydantic import BaseModel from instructor.function_calls import Mode -from tests.openai.util import models, modes +from ..util import models, modes class SinglePrediction(BaseModel): @@ -27,7 +27,7 @@ class SinglePrediction(BaseModel): @pytest.mark.parametrize("model, data, mode", product(models, data, modes)) @pytest.mark.asyncio async def test_classification(model, data, mode, aclient): - client = instructor.patch(aclient, mode=mode) + client = instructor.from_openai(aclient, mode=mode) if mode == instructor.Mode.JSON and model in {"gpt-3.5-turbo", "gpt-4"}: pytest.skip( @@ -50,7 +50,7 @@ async def test_classification(model, data, mode, aclient): # Adjust the prediction model to accommodate a list of labels class MultiClassPrediction(BaseModel): - predicted_labels: List[Literal["billing", "general_query", "hardware"]] + predicted_labels: list[Literal["billing", "general_query", "hardware"]] data = [ @@ -72,7 +72,7 @@ class MultiClassPrediction(BaseModel): @pytest.mark.parametrize("model, data, mode", product(models, data, modes)) @pytest.mark.asyncio async def test_multi_classify(model, data, mode, aclient): - client = instructor.patch(aclient, mode=mode) + client = instructor.from_openai(aclient, mode=mode) if (mode, model) in { (Mode.JSON, "gpt-3.5-turbo"), diff --git a/tests/openai/evals/test_entities.py b/tests/llm/test_openai/evals/test_entities.py similarity index 93% rename from tests/openai/evals/test_entities.py rename to tests/llm/test_openai/evals/test_entities.py index 8c74ab073..7943fef53 100644 --- a/tests/openai/evals/test_entities.py +++ b/tests/llm/test_openai/evals/test_entities.py @@ -1,12 +1,11 @@ from itertools import product -from typing import List from pydantic import BaseModel, Field import pytest import instructor from instructor.function_calls import Mode -from tests.openai.util import models, modes +from ..util import models, modes class Property(BaseModel): @@ -20,22 +19,22 @@ class Entity(BaseModel): ..., description="Unique identifier for the entity, used for deduplication, design a scheme allows multiple entities", ) - subquote_string: List[str] = Field( + subquote_string: list[str] = Field( ..., description="Correctly resolved value of the entity, if the entity is a reference to another entity, this should be the id of the referenced entity, include a few more words before and after the value to allow for some context to be used in the resolution", ) entity_title: str - properties: List[Property] = Field( + properties: list[Property] = Field( ..., description="List of properties of the entity" ) - dependencies: List[int] = Field( + dependencies: list[int] = Field( ..., description="List of entity ids that this entity depends or relies on to resolve it", ) class DocumentExtraction(BaseModel): - entities: List[Entity] = Field( + entities: list[Entity] = Field( ..., description="Body of the answer, each fact should be its seperate object with a body and a list of sources", ) diff --git a/tests/openai/evals/test_extract_users.py b/tests/llm/test_openai/evals/test_extract_users.py similarity index 96% rename from tests/openai/evals/test_extract_users.py rename to tests/llm/test_openai/evals/test_extract_users.py index 969fca339..a22a89b57 100644 --- a/tests/openai/evals/test_extract_users.py +++ b/tests/llm/test_openai/evals/test_extract_users.py @@ -3,7 +3,7 @@ from pydantic import BaseModel import instructor from instructor.function_calls import Mode -from tests.openai.util import models, modes +from ..util import models, modes class UserDetails(BaseModel): diff --git a/tests/llm/test_openai/evals/test_sentiment_analysis.py b/tests/llm/test_openai/evals/test_sentiment_analysis.py new file mode 100644 index 000000000..303e1dbd9 --- /dev/null +++ b/tests/llm/test_openai/evals/test_sentiment_analysis.py @@ -0,0 +1,60 @@ +import enum +from itertools import product +from pydantic import BaseModel +import pytest +import instructor +from instructor.function_calls import Mode +from ..util import models, modes + + +class Sentiment(str, enum.Enum): + POSITIVE = "positive" + NEGATIVE = "negative" + NEUTRAL = "neutral" + + +class SentimentAnalysis(BaseModel): + sentiment: Sentiment + + +test_data = [ + ( + "I absolutely love this product! It has exceeded all my expectations.", + Sentiment.POSITIVE, + ), + ( + "The service was terrible. I will never use this company again.", + Sentiment.NEGATIVE, + ), + ( + "The movie was okay. It had some good moments but overall it was average.", + Sentiment.NEUTRAL, + ), +] + + +@pytest.mark.parametrize("model, data, mode", product(models, test_data, modes)) +def test_sentiment_analysis(model, data, mode, client): + sample_data, expected_sentiment = data + + if (mode, model) in { + (Mode.JSON, "gpt-3.5-turbo"), + (Mode.JSON, "gpt-4"), + }: + pytest.skip(f"{mode} mode is not supported for {model}, skipping test") + + client = instructor.patch(client, mode=mode) + + response = client.chat.completions.create( + model=model, + response_model=SentimentAnalysis, + messages=[ + { + "role": "system", + "content": "You are a sentiment analysis model. Analyze the sentiment of the given text and provide the sentiment (positive, negative, or neutral).", + }, + {"role": "user", "content": sample_data}, + ], + ) + + assert response.sentiment == expected_sentiment diff --git a/tests/openai/test_modes.py b/tests/llm/test_openai/test_modes.py similarity index 92% rename from tests/openai/test_modes.py rename to tests/llm/test_openai/test_modes.py index bedddfd8a..08b30a010 100644 --- a/tests/openai/test_modes.py +++ b/tests/llm/test_openai/test_modes.py @@ -1,11 +1,10 @@ from itertools import product from pydantic import BaseModel, Field -from typing import List import pytest import instructor -from tests.openai.util import models, modes +from .util import models, modes class Item(BaseModel): @@ -14,7 +13,7 @@ class Item(BaseModel): class Order(BaseModel): - items: List[Item] = Field(..., default_factory=list) + items: list[Item] = Field(..., default_factory=list) customer: str @@ -56,7 +55,7 @@ class Book(BaseModel): class LibraryRecord(BaseModel): - books: List[Book] = Field(..., default_factory=list) + books: list[Book] = Field(..., default_factory=list) visitor: str library_id: str diff --git a/tests/openai/test_multitask.py b/tests/llm/test_openai/test_multitask.py similarity index 98% rename from tests/openai/test_multitask.py rename to tests/llm/test_openai/test_multitask.py index 70e51abc1..b6bb12b0f 100644 --- a/tests/openai/test_multitask.py +++ b/tests/llm/test_openai/test_multitask.py @@ -1,10 +1,10 @@ from itertools import product -from typing import Iterable +from collections.abc import Iterable from pydantic import BaseModel import pytest import instructor -from tests.openai.util import models, modes +from .util import models, modes class User(BaseModel): diff --git a/tests/openai/test_parallel.py b/tests/llm/test_openai/test_parallel.py similarity index 87% rename from tests/openai/test_parallel.py rename to tests/llm/test_openai/test_parallel.py index 16e901503..ed0f2fbf0 100644 --- a/tests/openai/test_parallel.py +++ b/tests/llm/test_openai/test_parallel.py @@ -1,4 +1,7 @@ -from typing import Iterable, Literal +from __future__ import annotations + +from typing import Literal, Union +from collections.abc import Iterable from pydantic import BaseModel import pytest @@ -32,7 +35,7 @@ def test_sync_parallel_tools__error(client): def test_sync_parallel_tools_or(client): - client = instructor.patch(client, mode=instructor.Mode.PARALLEL_TOOLS) + client = instructor.from_openai(client, mode=instructor.Mode.PARALLEL_TOOLS) resp = client.chat.completions.create( model="gpt-4-turbo-preview", messages=[ @@ -42,14 +45,14 @@ def test_sync_parallel_tools_or(client): "content": "What is the weather in toronto and dallas and who won the super bowl?", }, ], - response_model=Iterable[Weather | GoogleSearch], + response_model=Iterable[Union[Weather, GoogleSearch]], ) assert len(list(resp)) == 3 @pytest.mark.asyncio async def test_async_parallel_tools_or(aclient): - client = instructor.patch(aclient, mode=instructor.Mode.PARALLEL_TOOLS) + client = instructor.from_openai(aclient, mode=instructor.Mode.PARALLEL_TOOLS) resp = await client.chat.completions.create( model="gpt-4-turbo-preview", messages=[ @@ -59,7 +62,7 @@ async def test_async_parallel_tools_or(aclient): "content": "What is the weather in toronto and dallas and who won the super bowl?", }, ], - response_model=Iterable[Weather | GoogleSearch], + response_model=Iterable[Union[Weather, GoogleSearch]], ) assert len(list(resp)) == 3 diff --git a/tests/openai/test_patch.py b/tests/llm/test_openai/test_patch.py similarity index 98% rename from tests/openai/test_patch.py rename to tests/llm/test_openai/test_patch.py index a486936d4..be3db92a7 100644 --- a/tests/openai/test_patch.py +++ b/tests/llm/test_openai/test_patch.py @@ -4,7 +4,7 @@ import pytest import instructor -from tests.openai.util import models, modes +from .util import models, modes class UserExtract(BaseModel): diff --git a/tests/openai/test_retries.py b/tests/llm/test_openai/test_retries.py similarity index 98% rename from tests/openai/test_retries.py rename to tests/llm/test_openai/test_retries.py index f2ef0980a..b4297f9e1 100644 --- a/tests/openai/test_retries.py +++ b/tests/llm/test_openai/test_retries.py @@ -3,7 +3,7 @@ import pytest import instructor from itertools import product -from tests.openai.util import models, modes +from .util import models, modes def uppercase_validator(v): diff --git a/tests/openai/test_simple_types.py b/tests/llm/test_openai/test_simple_types.py similarity index 100% rename from tests/openai/test_simple_types.py rename to tests/llm/test_openai/test_simple_types.py diff --git a/tests/openai/test_stream.py b/tests/llm/test_openai/test_stream.py similarity index 97% rename from tests/openai/test_stream.py rename to tests/llm/test_openai/test_stream.py index 4b9b3b01b..4110cc2c4 100644 --- a/tests/openai/test_stream.py +++ b/tests/llm/test_openai/test_stream.py @@ -1,11 +1,11 @@ from itertools import product -from typing import Iterable +from collections.abc import Iterable from pydantic import BaseModel import pytest import instructor from instructor.dsl.partial import Partial -from tests.openai.util import models, modes +from .util import models, modes class UserExtract(BaseModel): diff --git a/tests/openai/test_validators.py b/tests/llm/test_openai/test_validators.py similarity index 80% rename from tests/openai/test_validators.py rename to tests/llm/test_openai/test_validators.py index 3b68393fc..3b447b5b5 100644 --- a/tests/openai/test_validators.py +++ b/tests/llm/test_openai/test_validators.py @@ -3,11 +3,11 @@ import instructor -from typing_extensions import Annotated +from typing import Annotated from pydantic import BaseModel, AfterValidator, BeforeValidator, ValidationError from instructor.dsl.validators import llm_validator -from tests.openai.util import models, modes +from .util import models, modes def test_patch_completes_successfully(client): @@ -22,7 +22,7 @@ class Response(BaseModel): @pytest.mark.parametrize("model, mode", product(models, modes)) def test_runmodel_validator_error(model, mode, client): - client = instructor.patch(client, mode=mode) + client = instructor.from_openai(client, mode=mode) class QuestionAnswerNoEvil(BaseModel): question: str @@ -30,7 +30,7 @@ class QuestionAnswerNoEvil(BaseModel): str, BeforeValidator( llm_validator( - "don't say objectionable things", model=model, openai_client=client + "don't say objectionable things", model=model, client=client ) ), ] @@ -43,13 +43,17 @@ class QuestionAnswerNoEvil(BaseModel): @pytest.mark.parametrize("model", models) -def test_runmodel_validator_default_openai_client(model): +def test_runmodel_validator_default_openai_client(model, client): + client = instructor.from_openai(client) + class QuestionAnswerNoEvil(BaseModel): question: str answer: Annotated[ str, BeforeValidator( - llm_validator("don't say objectionable things", model=model) + llm_validator( + "don't say objectionable things", model=model, client=client + ) ), ] diff --git a/tests/llm/test_openai/util.py b/tests/llm/test_openai/util.py new file mode 100644 index 000000000..7c515f948 --- /dev/null +++ b/tests/llm/test_openai/util.py @@ -0,0 +1,6 @@ +import instructor + +models = ["gpt-4-turbo"] +modes = [ + instructor.Mode.TOOLS, +] diff --git a/tests/openai/util.py b/tests/openai/util.py deleted file mode 100644 index b118e3e46..000000000 --- a/tests/openai/util.py +++ /dev/null @@ -1,7 +0,0 @@ -import instructor - -models = ["gpt-4-turbo-preview"] -modes = [ - instructor.Mode.JSON, - instructor.Mode.TOOLS, -] diff --git a/tests/test_distil.py b/tests/test_distil.py index 03eda3fec..7e3a3ab71 100644 --- a/tests/test_distil.py +++ b/tests/test_distil.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Callable, Tuple, cast +from typing import Any, Callable, cast import pytest import instructor @@ -93,7 +93,7 @@ def another_test_func(x: int) -> SimpleModel: # Mock track function for decorator tests -def mock_track(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> None: +def mock_track(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None: pass diff --git a/tests/test_function_calls.py b/tests/test_function_calls.py index c4fb9eada..07b2dc1da 100644 --- a/tests/test_function_calls.py +++ b/tests/test_function_calls.py @@ -1,18 +1,19 @@ -from typing import Type, TypeVar +from typing import TypeVar + import pytest -from pydantic import BaseModel +from anthropic.types import Message, Usage from openai.resources.chat.completions import ChatCompletion +from pydantic import BaseModel, ValidationError -from instructor import openai_schema, OpenAISchema import instructor +from instructor import OpenAISchema, openai_schema from instructor.exceptions import IncompleteOutputException - T = TypeVar("T") @pytest.fixture # type: ignore[misc] -def test_model() -> Type[OpenAISchema]: +def test_model() -> type[OpenAISchema]: class TestModel(OpenAISchema): # type: ignore[misc] name: str = "TestModel" data: str @@ -52,6 +53,26 @@ def mock_completion(request: T) -> ChatCompletion: return completion +@pytest.fixture # type: ignore[misc] +def mock_anthropic_message(request: T) -> Message: + data_content = '{\n"data": "Claude says hi"\n}' + if hasattr(request, "param"): + data_content = request.param.get("data_content", data_content) + return Message( + id="test_id", + content=[{"type": "text", "text": data_content}], + model="claude-3-haiku-20240307", + role="assistant", + stop_reason="end_turn", + stop_sequence=None, + type="message", + usage=Usage( + input_tokens=100, + output_tokens=100, + ), + ) + + def test_openai_schema() -> None: @openai_schema class Dataframe(BaseModel): # type: ignore[misc] @@ -96,14 +117,14 @@ class Dummy(OpenAISchema): # type: ignore[misc] indirect=True, ) # type: ignore[misc] def test_incomplete_output_exception( - test_model: Type[OpenAISchema], mock_completion: ChatCompletion + test_model: type[OpenAISchema], mock_completion: ChatCompletion ) -> None: with pytest.raises(IncompleteOutputException): test_model.from_response(mock_completion, mode=instructor.Mode.FUNCTIONS) def test_complete_output_no_exception( - test_model: Type[OpenAISchema], mock_completion: ChatCompletion + test_model: type[OpenAISchema], mock_completion: ChatCompletion ) -> None: test_model_instance = test_model.from_response( mock_completion, mode=instructor.Mode.FUNCTIONS @@ -117,8 +138,51 @@ def test_complete_output_no_exception( [{"finish_reason": "length", "data_content": '{\n"data": "incomplete dat"\n}'}], indirect=True, ) # type: ignore[misc] -async def test_incomplete_output_exception_raise( - test_model: Type[OpenAISchema], mock_completion: ChatCompletion +def test_incomplete_output_exception_raise( + test_model: type[OpenAISchema], mock_completion: ChatCompletion ) -> None: with pytest.raises(IncompleteOutputException): - await test_model.from_response(mock_completion, mode=instructor.Mode.FUNCTIONS) + test_model.from_response(mock_completion, mode=instructor.Mode.FUNCTIONS) + + +def test_anthropic_no_exception( + test_model: type[OpenAISchema], mock_anthropic_message: Message +) -> None: + test_model_instance = test_model.from_response( + mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON + ) + assert test_model_instance.data == "Claude says hi" + + +@pytest.mark.parametrize( + "mock_anthropic_message", + [{"data_content": '{\n"data": "Claude likes\ncontrol\ncharacters"\n}'}], + indirect=True, +) # type: ignore[misc] +def test_control_characters_not_allowed_in_anthropic_json_strict_mode( + test_model: type[OpenAISchema], mock_anthropic_message: Message +) -> None: + with pytest.raises(ValidationError) as exc_info: + test_model.from_response( + mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON, strict=True + ) + + # https://docs.pydantic.dev/latest/errors/validation_errors/#json_invalid + exc = exc_info.value + assert len(exc.errors()) == 1 + assert exc.errors()[0]["type"] == "json_invalid" + assert "control character" in exc.errors()[0]["msg"] + + +@pytest.mark.parametrize( + "mock_anthropic_message", + [{"data_content": '{\n"data": "Claude likes\ncontrol\ncharacters"\n}'}], + indirect=True, +) # type: ignore[misc] +def test_control_characters_allowed_in_anthropic_json_non_strict_mode( + test_model: type[OpenAISchema], mock_anthropic_message: Message +) -> None: + test_model_instance = test_model.from_response( + mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON, strict=False + ) + assert test_model_instance.data == "Claude likes\ncontrol\ncharacters" diff --git a/tests/test_patch.py b/tests/test_patch.py index b0e72e58a..2a25c6e53 100644 --- a/tests/test_patch.py +++ b/tests/test_patch.py @@ -3,7 +3,7 @@ from openai import AsyncOpenAI, OpenAI import instructor -from instructor.patch import OVERRIDE_DOCS, is_async +from instructor.utils import is_async def test_patch_completes_successfully(): @@ -71,9 +71,3 @@ def triple_wrapped_function(): pass assert is_async(triple_wrapped_function) is True - - -def test_override_docs(): - assert ( - "response_model" in OVERRIDE_DOCS - ), "response_model should be in OVERRIDE_DOCS" diff --git a/tests/test_simple_types.py b/tests/test_simple_types.py index 05239d200..4add37bd1 100644 --- a/tests/test_simple_types.py +++ b/tests/test_simple_types.py @@ -53,16 +53,8 @@ def test_union_simple(): def test_iterable_not_simple(): - from typing import Iterable + from collections.abc import Iterable new_type = Iterable[int] assert not is_simple_type(new_type), "Failed for type: " + str(new_type) - - -def test_list_is_simple(): - from typing import List - - new_type = List[int] - - assert is_simple_type(new_type), "Failed for type: " + str(new_type) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..217ce604d --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,193 @@ +import json +import pytest +from instructor.utils import ( + classproperty, + extract_json_from_codeblock, + extract_json_from_stream, + extract_json_from_stream_async, + merge_consecutive_messages, +) + + +def test_extract_json_from_codeblock(): + example = """ + Here is a response + + ```json + { + "key": "value" + } + ``` + """ + result = extract_json_from_codeblock(example) + assert json.loads(result) == {"key": "value"} + + +def test_extract_json_from_codeblock_no_end(): + example = """ + Here is a response + + ```json + { + "key": "value", + "another_key": [{"key": {"key": "value"}}] + } + """ + result = extract_json_from_codeblock(example) + assert json.loads(result) == { + "key": "value", + "another_key": [{"key": {"key": "value"}}], + } + + +def test_extract_json_from_codeblock_no_start(): + example = """ + Here is a response + + { + "key": "value", + "another_key": [{"key": {"key": "value"}}, {"key": "value"}] + } + """ + result = extract_json_from_codeblock(example) + assert json.loads(result) == { + "key": "value", + "another_key": [{"key": {"key": "value"}}, {"key": "value"}], + } + + +def test_stream_json(): + text = """here is the json for you! + + ```json + , here + { + "key": "value", + "another_key": [{"key": {"key": "value"}}] + } + ``` + + What do you think? + """ + + def batch_strings(chunks, n=2): + batch = "" + for chunk in chunks: + for char in chunk: + batch += char + if len(batch) == n: + yield batch + batch = "" + if batch: # Yield any remaining characters in the last batch + yield batch + + result = json.loads( + "".join(list(extract_json_from_stream(batch_strings(text, n=3)))) + ) + assert result == {"key": "value", "another_key": [{"key": {"key": "value"}}]} + + +@pytest.mark.asyncio +async def test_stream_json_async(): + text = """here is the json for you! + + ```json + , here + { + "key": "value", + "another_key": [{"key": {"key": "value"}}, {"key": "value"}] + } + ``` + + What do you think? + """ + + async def batch_strings_async(chunks, n=2): + batch = "" + for chunk in chunks: + for char in chunk: + batch += char + if len(batch) == n: + yield batch + batch = "" + if batch: # Yield any remaining characters in the last batch + yield batch + + result = json.loads( + "".join( + [ + chunk + async for chunk in extract_json_from_stream_async( + batch_strings_async(text, n=3) + ) + ] + ) + ) + assert result == { + "key": "value", + "another_key": [{"key": {"key": "value"}}, {"key": "value"}], + } + + +def test_merge_consecutive_messages(): + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "user", "content": "How are you"}, + {"role": "assistant", "content": "Hello"}, + {"role": "assistant", "content": "I am good"}, + ] + result = merge_consecutive_messages(messages) + assert result == [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "How are you"}, + ], + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "I am good"}, + ], + }, + ] + + +def test_merge_consecutive_messages_empty(): + messages = [] + result = merge_consecutive_messages(messages) + assert result == [] + + +def test_merge_consecutive_messages_single(): + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hello"}, + ] + result = merge_consecutive_messages(messages) + assert result == [ + {"role": "user", "content": [{"type": "text", "text": "Hello"}]}, + {"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}, + ] + + +def test_classproperty(): + """Test custom `classproperty` descriptor.""" + + class MyClass: + @classproperty + def my_property(cls): + return cls + + assert MyClass.my_property is MyClass + + class MyClass: + clvar = 1 + + @classproperty + def my_property(cls): + return cls.clvar + + assert MyClass.my_property == 1