Repository: guardrails-ai/guardrails (language: python)

Issue:
[bug] Type annotated objects in pydantic guards incorrectly transpile to XML
**Describe the bug**
When making a guard from a pydantic model, if a Dict has type arguments on it, it transpiles into XML in a way that doesn't work.

Relevant Discord thread:
https://discord.com/channels/1085077079697150023/1149032565844475965/1149296871517470810

**To Reproduce**
```python
from pydantic import BaseModel
from guardrails import Guard

class CorrectAnswers(BaseModel):
    all_of: Dict[str, str] = {}
    any_of_groups: List[Dict[str, str]] = []
    not_all_of_groups: List[Dict[str, str]] = []
    not_any_of: Dict[str, str] = {}

Guard.from_pydantic(CorrectAnswers)
```

**Expected behavior**
XML guards don't support specifying types in objects with arbitrary elements, so it should probably just act like this:
```python
class CorrectAnswers(BaseModel):
    all_of: Dict = {}
    any_of_groups: List[Dict] = []
    not_all_of_groups: List[Dict] = []
    not_any_of: Dict = {}
```

**Library version:**
0.2.0

**Additional context**
This bug will probably be fixed when we migrate to XML-invariant internals.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c3a2f94e1..fb1816b86 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,6 @@ jobs:
         strategy:
             matrix:
                 python-version: ['3.8', '3.9', '3.10', '3.11']
-        
         steps:
             -   uses: actions/checkout@v2
             -   name: Set up Python ${{ matrix.python-version }}
@@ -51,7 +50,7 @@ jobs:
         strategy:
             matrix:
                 python-version: ['3.8', '3.9', '3.10', '3.11']
-
+                pydantic-version: ['1.10.9', '2.4.2']
         steps:
             -   uses: actions/checkout@v2
             -   name: Set up Python ${{ matrix.python-version }}
@@ -63,7 +62,7 @@ jobs:
                 uses: actions/cache@v3
                 with:
                     path: ~/.cache/pypoetry
-                    key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ env.POETRY_VERSION }}
+                    key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ env.POETRY_VERSION }}-${{ matrix.pydantic-version }}
 
             -   name: Install Poetry
                 uses: snok/install-poetry@v1
@@ -72,10 +71,17 @@ jobs:
                 # TODO: fix errors so that we can run `make dev` instead
                 run: |
                     make full
+                    poetry run pip install pydantic==${{ matrix.pydantic-version }}
+
+            -   if: matrix.pydantic-version == '2.4.2'
+                name: Static analysis with pyright (ignoring pydantic v1)
+                run: |
+                    make type-pydantic-v2
 
-            -   name: Static analysis with pyright
+            -   if: matrix.pydantic-version == '1.10.9'
+                name: Static analysis with mypy (ignoring pydantic v2)
                 run: |
-                    make type
+                    make type-pydantic-v1
 
     Pytests:
         runs-on: ubuntu-latest
@@ -85,7 +91,7 @@ jobs:
                 # TODO: fix errors so that we can run both `make dev` and `make full`
                 # dependencies: ['dev', 'full']
                 dependencies: ['full']
-
+                pydantic-version: ['1.10.9', '2.4.2']
         steps:
             -   uses: actions/checkout@v2
             -   name: Set up Python ${{ matrix.python-version }}
@@ -97,15 +103,15 @@ jobs:
                 uses: actions/cache@v3
                 with:
                     path: ~/.cache/pypoetry
-                    key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ env.POETRY_VERSION }}
+                    key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ env.POETRY_VERSION }}-${{ matrix.pydantic-version }}
 
             -   name: Install Poetry
                 uses: snok/install-poetry@v1
 
             -   name: Install Dependencies
                 run: |
-                    python -m pip install --upgrade pip
                     make ${{ matrix.dependencies }}
+                    python -m pip install pydantic==${{ matrix.pydantic-version }}
 
             -   name: Run Pytests
                 run: |
diff --git a/Makefile b/Makefile
index a70387577..d09efa608 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,16 @@ autoformat:
 type:
 	poetry run pyright guardrails/
 
+type-pydantic-v1:
+	echo '{"exclude": ["guardrails/utils/pydantic_utils/v2.py"]}' > pyrightconfig.json
+	poetry run pyright guardrails/
+	rm pyrightconfig.json
+
+type-pydantic-v2:
+	echo '{"exclude": ["guardrails/utils/pydantic_utils/v1.py"]}' > pyrightconfig.json
+	poetry run pyright guardrails/
+	rm pyrightconfig.json
+
 lint:
 	poetry run isort -c guardrails/ tests/
 	poetry run black guardrails/ tests/ --check
@@ -44,4 +54,4 @@ all: autoformat type lint docs test
 precommit:
 	# pytest -x -q --no-summary
 	pyright guardrails/
-	make lint
\ No newline at end of file
+	make lint
diff --git a/docs/examples/check_for_pii.ipynb b/docs/examples/check_for_pii.ipynb
new file mode 100644
index 000000000..5cd6a0856
--- /dev/null
+++ b/docs/examples/check_for_pii.ipynb
@@ -0,0 +1,267 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Check whether an LLM response contains PII (Personally Identifiable Information)\n",
+    "\n",
+    "**Using the `PIIFilter` validator**\n",
+    "\n",
+    "This is a simple check that looks for the presence of a few common PII patterns\n",
+    "It is not intended to be a comprehensive check for PII and to be a quick check that can be used to filter out responses that are likely to contain PII. It uses the Microsoft Presidio library to check for PII.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3\u001b[0m\n",
+      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n",
+      "\n",
+      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3\u001b[0m\n",
+      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n",
+      "\u001b[38;5;2m✔ Download and installation successful\u001b[0m\n",
+      "You can now load the package via spacy.load('en_core_web_lg')\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Install the necessary packages\n",
+    "! pip install presidio-analyzer presidio-anonymizer -q\n",
+    "! python -m spacy download en_core_web_lg -q"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Import the guardrails package\n",
+    "import guardrails as gd\n",
+    "from guardrails.validators import PIIFilter\n",
+    "from rich import print"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/Users/karanacharya/guardrails-ai/guardrails/guardrails/rail.py:115: UserWarning: Prompt must be provided during __call__.\n",
+      "  warnings.warn(\"Prompt must be provided during __call__.\")\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Create Guard object with this validator\n",
+    "# One can specify either pre-defined set of PII or SPI (Sensitive Personal Information) entities by passing in the `pii` or `spi` argument respectively.\n",
+    "# It can be passed either durring intialization or later through the metadata argument in parse method.\n",
+    "\n",
+    "# One can also pass in a list of entities supported by Presidio to the `pii_entities` argument.\n",
+    "guard = gd.Guard.from_string(\n",
+    "    validators=[PIIFilter(pii_entities=\"pii\", on_fail=\"fix\")],\n",
+    "    description=\"testmeout\",\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">My email address is <span style=\"font-weight: bold\">&lt;</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">EMAIL_ADDRESS</span><span style=\"color: #000000; text-decoration-color: #000000\">&gt;, and my phone number is &lt;PHONE_NUMBER</span><span style=\"font-weight: bold\">&gt;</span>\n",
+       "</pre>\n"
+      ],
+      "text/plain": [
+       "My email address is \u001b[1m<\u001b[0m\u001b[1;95mEMAIL_ADDRESS\u001b[0m\u001b[39m>, and my phone number is <PHONE_NUMBER\u001b[0m\u001b[1m>\u001b[0m\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Parse the text\n",
+    "text = \"My email address is demo@lol.com, and my phone number is 1234567890\"\n",
+    "output = guard.parse(\n",
+    "    llm_output=text,\n",
+    ")\n",
+    "\n",
+    "# Print the output\n",
+    "print(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here, both EMAIL_ADDRESS and PHONE_NUMBER are detected as PII.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">My email address is <span style=\"font-weight: bold\">&lt;</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">EMAIL_ADDRESS</span><span style=\"font-weight: bold\">&gt;</span>, and my phone number is <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">1234567890</span>\n",
+       "</pre>\n"
+      ],
+      "text/plain": [
+       "My email address is \u001b[1m<\u001b[0m\u001b[1;95mEMAIL_ADDRESS\u001b[0m\u001b[1m>\u001b[0m, and my phone number is \u001b[1;36m1234567890\u001b[0m\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Let's test with passing through metadata for the same guard object\n",
+    "# This will take precendence over the entities passed in during initialization\n",
+    "output = guard.parse(\n",
+    "    llm_output=text,\n",
+    "    metadata={\"pii_entities\": [\"EMAIL_ADDRESS\"]},\n",
+    ")\n",
+    "\n",
+    "# Print the output\n",
+    "print(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "As you can see here, only EMAIL_ADDRESS is detected as PII, and the PHONE_NUMBER is not detected as PII.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Let's try with SPI entities\n",
+    "# Create a new guard object\n",
+    "guard = gd.Guard.from_string(\n",
+    "    validators=[PIIFilter(pii_entities=\"spi\", on_fail=\"fix\")],\n",
+    "    description=\"testmeout\",\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">My email address is demo@xyz.com, and my account number is <span style=\"font-weight: bold\">&lt;</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">US_BANK_NUMBER</span><span style=\"font-weight: bold\">&gt;</span>.\n",
+       "</pre>\n"
+      ],
+      "text/plain": [
+       "My email address is demo@xyz.com, and my account number is \u001b[1m<\u001b[0m\u001b[1;95mUS_BANK_NUMBER\u001b[0m\u001b[1m>\u001b[0m.\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Parse text\n",
+    "text = \"My email address is demo@xyz.com, and my account number is 1234789012367654.\"\n",
+    "\n",
+    "output = guard.parse(\n",
+    "    llm_output=text,\n",
+    ")\n",
+    "\n",
+    "# Print the output\n",
+    "print(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here, only the US_BANK_NUMBER is detected as PII, as specified in the \"spi\" entities. Refer to the documentation for more information on the \"pii\" and \"spi\" entities. Obviosuly, you can pass in any [Presidio-supported entities](https://microsoft.github.io/presidio/supported_entities/) through the metadata.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">My ITIN is <span style=\"font-weight: bold\">&lt;</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">US_ITIN</span><span style=\"color: #000000; text-decoration-color: #000000\">&gt; and my driver's license number is &lt;US_DRIVER_LICENSE</span><span style=\"font-weight: bold\">&gt;</span>\n",
+       "</pre>\n"
+      ],
+      "text/plain": [
+       "My ITIN is \u001b[1m<\u001b[0m\u001b[1;95mUS_ITIN\u001b[0m\u001b[39m> and my driver's license number is <US_DRIVER_LICENSE\u001b[0m\u001b[1m>\u001b[0m\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Another example\n",
+    "text = \"My ITIN is 923756789 and my driver's license number is 87651239\"\n",
+    "\n",
+    "output = guard.parse(\n",
+    "    llm_output=text,\n",
+    "    metadata={\"pii_entities\": [\"US_ITIN\", \"US_DRIVER_LICENSE\"]},\n",
+    ")\n",
+    "\n",
+    "# Print the output\n",
+    "print(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### In this way, any PII entity that you want to check for can be passed in through the metadata and masked by Guardrails for your LLM outputs. Of-course, like all other examples, you can integrate this into your own code and workflows through the complete Guard execution.\n"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "guard-venv",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/guardrails/utils/parsing_utils.py b/guardrails/utils/parsing_utils.py
index 3309b7c74..062727b63 100644
--- a/guardrails/utils/parsing_utils.py
+++ b/guardrails/utils/parsing_utils.py
@@ -65,7 +65,7 @@ def get_code_block(
 
 def get_template_variables(template: str) -> List[str]:
     if hasattr(Template, "get_identifiers"):
-        return Template(template).get_identifiers()
+        return Template(template).get_identifiers()  # type: ignore
     else:
         d = collections.defaultdict(str)
         Template(template).safe_substitute(d)
diff --git a/guardrails/utils/pydantic_utils/__init__.py b/guardrails/utils/pydantic_utils/__init__.py
index 8e7fee341..4195ff919 100644
--- a/guardrails/utils/pydantic_utils/__init__.py
+++ b/guardrails/utils/pydantic_utils/__init__.py
@@ -11,7 +11,13 @@
         convert_pydantic_model_to_openai_fn,
     )
 else:
-    raise NotImplementedError(f"Pydantic version {PYDANTIC_VERSION} is not supported.")
+    from .v2 import (
+        ArbitraryModel,
+        add_pydantic_validators_as_guardrails_validators,
+        add_validator,
+        convert_pydantic_model_to_datatype,
+        convert_pydantic_model_to_openai_fn,
+    )
 
 
 __all__ = [
diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py
index 7dbd46409..e17e6a7de 100644
--- a/guardrails/utils/pydantic_utils/v1.py
+++ b/guardrails/utils/pydantic_utils/v1.py
@@ -311,22 +311,22 @@ def convert_pydantic_model_to_datatype(
             inner_type = get_args(type_annotation)
             if len(inner_type) == 0:
                 # If the list is empty, we cannot infer the type of the elements
-                children[field_name] = datatype_to_pydantic_field(
+                children[field_name] = pydantic_field_to_datatype(
                     ListDataType,
                     field,
                     strict=strict,
                 )
+                continue
             inner_type = inner_type[0]
             if is_pydantic_base_model(inner_type):
                 child = convert_pydantic_model_to_datatype(inner_type)
             else:
                 inner_target_datatype = field_to_datatype(inner_type)
-                child = datatype_to_pydantic_field(
+                child = construct_datatype(
                     inner_target_datatype,
-                    inner_type,
                     strict=strict,
                 )
-            children[field_name] = datatype_to_pydantic_field(
+            children[field_name] = pydantic_field_to_datatype(
                 ListDataType,
                 field,
                 children={"item": child},
@@ -349,7 +349,7 @@ def convert_pydantic_model_to_datatype(
                     strict=strict,
                     excluded_fields=[discriminator],
                 )
-            children[field_name] = datatype_to_pydantic_field(
+            children[field_name] = pydantic_field_to_datatype(
                 Choice,
                 field,
                 children=choice_children,
@@ -361,31 +361,28 @@ def convert_pydantic_model_to_datatype(
                 field, datatype=target_datatype, strict=strict
             )
         else:
-            children[field_name] = datatype_to_pydantic_field(
+            children[field_name] = pydantic_field_to_datatype(
                 target_datatype,
                 field,
                 strict=strict,
             )
 
     if isinstance(model_field, ModelField):
-        return datatype_to_pydantic_field(
+        return pydantic_field_to_datatype(
             datatype,
             model_field,
             children=children,
             strict=strict,
         )
     else:
-        format_attr = FormatAttr.from_validators([], ObjectDataType.tag, strict)
-        return datatype(
+        return construct_datatype(
+            datatype,
             children=children,
-            format_attr=format_attr,
-            optional=False,
             name=name,
-            description=None,
         )
 
 
-def datatype_to_pydantic_field(
+def pydantic_field_to_datatype(
     datatype: Type[T],
     field: ModelField,
     children: Optional[Dict[str, "DataType"]] = None,
@@ -396,14 +393,38 @@ def datatype_to_pydantic_field(
         children = {}
 
     validators = field.field_info.extra.get("validators", [])
-    format_attr = FormatAttr.from_validators(validators, datatype.tag, strict)
 
     is_optional = field.required is False
 
     name = field.name
     description = field.field_info.description
 
-    data_type = datatype(
-        children, format_attr, is_optional, name, description, **kwargs
+    return construct_datatype(
+        datatype,
+        children,
+        validators,
+        is_optional,
+        name,
+        description,
+        strict=strict,
+        **kwargs,
     )
-    return data_type
+
+
+def construct_datatype(
+    datatype: Type[T],
+    children: Optional[Dict[str, Any]] = None,
+    validators: Optional[typing.List[Validator]] = None,
+    optional: bool = False,
+    name: Optional[str] = None,
+    description: Optional[str] = None,
+    strict: bool = False,
+    **kwargs,
+) -> T:
+    if children is None:
+        children = {}
+    if validators is None:
+        validators = []
+
+    format_attr = FormatAttr.from_validators(validators, datatype.tag, strict)
+    return datatype(children, format_attr, optional, name, description, **kwargs)
diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py
new file mode 100644
index 000000000..ff360afb0
--- /dev/null
+++ b/guardrails/utils/pydantic_utils/v2.py
@@ -0,0 +1,466 @@
+import typing
+import warnings
+from copy import deepcopy
+from datetime import date, time
+from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, get_args
+
+from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
+from pydantic.fields import FieldInfo
+
+from guardrails.datatypes import URL as URLDataType
+from guardrails.datatypes import Boolean as BooleanDataType
+from guardrails.datatypes import Case as CaseDataType
+from guardrails.datatypes import Choice
+from guardrails.datatypes import Choice as ChoiceDataType
+from guardrails.datatypes import DataType
+from guardrails.datatypes import Date as DateDataType
+from guardrails.datatypes import Float as FloatDataType
+from guardrails.datatypes import Integer as IntegerDataType
+from guardrails.datatypes import List as ListDataType
+from guardrails.datatypes import Object as ObjectDataType
+from guardrails.datatypes import PythonCode as PythonCodeDataType
+from guardrails.datatypes import String as StringDataType
+from guardrails.datatypes import Time as TimeDataType
+from guardrails.formatattr import FormatAttr
+from guardrails.validator_base import Validator
+
+DataTypeT = TypeVar("DataTypeT", bound=DataType)
+
+
+class ArbitraryModel(BaseModel):
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+
+def add_validator(
+    __field: str,
+    *fields: str,
+    fn: Callable,
+    **kwargs,
+) -> Callable:
+    if kwargs:
+        warnings.warn(
+            "The following kwargs are not supported by pydantic v2 "
+            "and will be ignored: "
+            f"{kwargs}"
+        )
+    return field_validator(
+        __field,
+        *fields,
+    )(fn)
+
+
+def is_pydantic_base_model(type_annotation: Any) -> Union[Type[BaseModel], None]:
+    """Check if a type_annotation is a Pydantic BaseModel."""
+    if (
+        type_annotation is not None
+        and isinstance(type_annotation, type)
+        and issubclass(type_annotation, BaseModel)
+    ):
+        return type_annotation
+    return None
+
+
+def is_list(type_annotation: Any) -> bool:
+    """Check if a type_annotation is a list."""
+
+    type_annotation = prepare_type_annotation(type_annotation)
+
+    if is_pydantic_base_model(type_annotation):
+        return False
+    if typing.get_origin(type_annotation) == list:
+        return True
+    elif type_annotation == list:
+        return True
+    return False
+
+
+def is_dict(type_annotation: Any) -> bool:
+    """Check if a type_annotation is a dict."""
+
+    type_annotation = prepare_type_annotation(type_annotation)
+
+    if is_pydantic_base_model(type_annotation):
+        return True
+    if typing.get_origin(type_annotation) == dict:
+        return True
+    elif type_annotation == dict:
+        return True
+    return False
+
+
+def _create_bare_model(model: Type[BaseModel]) -> Type[BaseModel]:
+    class BareModel(BaseModel):
+        __annotations__ = getattr(model, "__annotations__", {})
+
+    return BareModel
+
+
+def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict:
+    """Convert a Pydantic BaseModel to an OpenAI function.
+
+    Args:
+        model: The Pydantic BaseModel to convert.
+
+    Returns:
+        OpenAI function paramters.
+    """
+
+    bare_model = _create_bare_model(type(model))
+
+    # Convert Pydantic model to JSON schema
+    json_schema = bare_model.model_json_schema()
+
+    # Create OpenAI function parameters
+    fn_params = {
+        "name": json_schema["title"],
+        "parameters": json_schema,
+    }
+    if "description" in json_schema and json_schema["description"] is not None:
+        fn_params["description"] = json_schema["description"]
+
+    return fn_params
+
+
+def convert_pydantic_validator_to_guardrails_validator(
+    model: Type[BaseModel], fn: Callable
+) -> Union[str, Validator]:
+    """Convert a Pydantic validator to a Guardrails validator.
+
+    Pydantic validators can be defined in three ways:
+        1. A method defined in the BaseModel using a `validator` decorator.
+        2. Using the `add_validator` function with a Guardrails validator class.
+        3. Using the `add_validator` function with a custom function.
+    This method converts all three types of validators to a Guardrails validator.
+
+    Args:
+        model: The Pydantic BaseModel that the validator is defined in.
+        fn: The Pydantic validator function. This is the raw cython function generated
+            by calling `BaseModelName.__fields__[field_name].post_validators[idx]`.
+
+    Returns:
+        A Guardrails validator
+    """
+
+    fn_name = fn.__name__
+    callable_fn = fn.__wrapped__
+
+    if hasattr(model, fn_name):
+        # # Case 1: fn is a method defined in the BaseModel
+        # # Wrap the method in a Guardrails PydanticFieldValidator class
+        # field_validator = partial(callable_fn, model)
+        # return PydanticFieldValidator(field_validator=field_validator)
+        warnings.warn(
+            f"Validator {fn_name} is defined as a method in the BaseModel. "
+            "This is not supported by Guardrails. "
+            "Please define the validator using the `add_validator` function."
+        )
+        return fn_name
+
+    if issubclass(type(callable_fn), Validator):
+        # Case 2: fn is a Guardrails validator
+        return callable_fn
+    else:
+        # # Case 3: fn is a custom function
+        # return PydanticFieldValidator(field_validator=callable_fn)
+        warnings.warn(
+            f"Validator {fn_name} is defined as a custom function. "
+            "This is not supported by Guardrails. "
+            "Please define the validator using the `add_validator` function."
+        )
+        return fn_name
+
+
+def add_pydantic_validators_as_guardrails_validators(
+    model: Type[BaseModel],
+) -> Dict[str, FieldInfo]:
+    """Extract all validators for a pydantic BaseModel.
+
+    This function converts each Pydantic validator to a GuardRails validator and adds
+    it to the corresponding field in the model. The resulting dictionary maps field
+    names to FieldInfo objects.
+
+    Args:
+        model: A pydantic BaseModel.
+
+    Returns:
+        A dictionary mapping field names to FieldInfo objects.
+    """
+
+    def process_validators(vals, fld):
+        if not vals:
+            return
+
+        for val in vals:
+            gd_validator = convert_pydantic_validator_to_guardrails_validator(
+                model, val
+            )
+            if "validators" not in fld.field_info.json_schema_extra:
+                fld.json_schema_extra["validators"] = []
+            fld.json_schema_extra["validators"].append((gd_validator, "reask"))
+
+    model_fields = {}
+    for field_name, field in model.model_fields.items():
+        field_copy = deepcopy(field)
+        if field.json_schema_extra is None:
+            field_copy.json_schema_extra = {}
+
+        if (
+            field_copy.json_schema_extra is not None
+            and isinstance(field_copy.json_schema_extra, dict)
+            and "validators" in field_copy.json_schema_extra
+            and not isinstance(field_copy.json_schema_extra["validators"], list)
+        ):
+            field_copy.json_schema_extra["validators"] = [
+                field_copy.json_schema_extra["validators"]
+            ]
+
+        # TODO figure out how to process pydantic2 validators;
+        #  they're abstracted into pydantic's rust core
+        # process_validators(field.pre_validators, field_copy)
+        # process_validators(field.post_validators, field_copy)
+        model_fields[field_name] = field_copy
+
+    # TODO(shreya): Before merging handle root validators
+    return model_fields
+
+
+def prepare_type_annotation(type_annotation: Union[FieldInfo, Type]) -> Type:
+    """Get the raw type annotation that can be used for downstream processing.
+
+    This function does the following:
+        1. If the type_annotation is a Pydantic field, get the annotation
+        2. If the type_annotation is a Union, get the first non-None type
+
+    Args:
+        type_annotation (Any): The type annotation to prepare
+
+    Returns:
+        Type: The prepared type annotation
+    """
+
+    if isinstance(type_annotation, FieldInfo):
+        type_annotation = type_annotation.annotation
+
+    # Strip a Union type annotation to the first non-None type
+    if typing.get_origin(type_annotation) == Union:
+        non_none_type_annotation = [
+            t for t in get_args(type_annotation) if t != type(None)  # noqa E721
+        ]
+        if len(non_none_type_annotation) == 1:
+            return non_none_type_annotation[0]
+        return type_annotation
+
+    return type_annotation
+
+
+def field_to_datatype(field: Union[FieldInfo, Type]) -> Type[DataType]:
+    """Map a type_annotation to the name of the corresponding field type.
+
+    This function checks if the type_annotation is a list, dict, or a
+    primitive type, and returns the corresponding type name, e.g.
+    "list", "object", "bool", "date", etc.
+    """
+
+    # FIXME: inaccessible datatypes:
+    #   - Email
+    #   - SQLCode
+    #   - Percentage
+
+    # Get the type annotation from the type_annotation
+    type_annotation = prepare_type_annotation(field)
+
+    # Use inline import to avoid circular dependency
+    from guardrails.datatypes import PythonCode
+
+    # Map the type annotation to the corresponding field type
+    if is_list(type_annotation):
+        return ListDataType
+    elif is_dict(type_annotation):
+        return ObjectDataType
+    elif type_annotation == bool:
+        return BooleanDataType
+    elif type_annotation == date:
+        return DateDataType
+    elif type_annotation == float:
+        return FloatDataType
+    elif type_annotation == int:
+        return IntegerDataType
+    elif type_annotation == str or typing.get_origin(type_annotation) == typing.Literal:
+        return StringDataType
+    elif type_annotation == time:
+        return TimeDataType
+    elif type_annotation == HttpUrl:
+        return URLDataType
+    elif typing.get_origin(type_annotation) == Union:
+        return ChoiceDataType
+    elif type_annotation == PythonCode:
+        return PythonCodeDataType
+    else:
+        raise ValueError(f"Unsupported type: {type_annotation}")
+
+
+def convert_pydantic_model_to_datatype(
+    model_field: typing.Union[FieldInfo, Type[BaseModel]],
+    datatype: Type[DataTypeT] = ObjectDataType,
+    excluded_fields: Optional[List[str]] = None,
+    name: Optional[str] = None,
+    strict: bool = False,
+) -> DataTypeT:
+    """Create an Object from a Pydantic model."""
+    if excluded_fields is None:
+        excluded_fields = []
+
+    if isinstance(model_field, FieldInfo):
+        model = model_field.annotation
+        model = is_pydantic_base_model(model)
+        if not model:
+            raise ValueError(
+                f"Expected a Pydantic model, but got {model_field.annotation}"
+            )
+    else:
+        model = model_field
+
+    model_fields = add_pydantic_validators_as_guardrails_validators(model)
+
+    children = {}
+    for field_name, field in model_fields.items():
+        if field_name in excluded_fields:
+            continue
+        type_annotation = prepare_type_annotation(field)
+        target_datatype = field_to_datatype(field)
+        if target_datatype == ListDataType:
+            inner_type = get_args(type_annotation)
+            if len(inner_type) == 0:
+                # If the list is empty, we cannot infer the type of the elements
+                children[field_name] = pydantic_field_to_datatype(
+                    ListDataType,
+                    field,
+                    strict=strict,
+                )
+                continue
+            inner_type = inner_type[0]
+            if is_pydantic_base_model(inner_type):
+                child = convert_pydantic_model_to_datatype(
+                    inner_type,
+                )
+            else:
+                inner_target_datatype = field_to_datatype(inner_type)
+                child = construct_datatype(
+                    inner_target_datatype,
+                    strict=strict,
+                    name=field_name,
+                )
+            children[field_name] = pydantic_field_to_datatype(
+                ListDataType,
+                field,
+                children={"item": child},
+                strict=strict,
+                name=field_name,
+            )
+        elif target_datatype == ChoiceDataType:
+            discriminator = field.discriminator or "discriminator"
+            choice_children = {}
+            for case in typing.get_args(field.annotation):
+                case_discriminator_type = case.model_fields[discriminator].annotation
+                assert typing.get_origin(case_discriminator_type) is typing.Literal
+                assert len(typing.get_args(case_discriminator_type)) == 1
+                discriminator_value = typing.get_args(case_discriminator_type)[0]
+                choice_children[
+                    discriminator_value
+                ] = convert_pydantic_model_to_datatype(
+                    case,
+                    datatype=CaseDataType,
+                    name=discriminator_value,
+                    strict=strict,
+                    excluded_fields=[discriminator],
+                )
+            children[field_name] = pydantic_field_to_datatype(
+                Choice,
+                field,
+                children=choice_children,
+                strict=strict,
+                discriminator_key=discriminator,
+                name=field_name,
+            )
+        elif is_pydantic_base_model(field.annotation):
+            children[field_name] = convert_pydantic_model_to_datatype(
+                field,
+                datatype=target_datatype,
+                strict=strict,
+                name=field_name,
+            )
+        else:
+            children[field_name] = pydantic_field_to_datatype(
+                target_datatype,
+                field,
+                strict=strict,
+                name=field_name,
+            )
+
+    if isinstance(model_field, FieldInfo):
+        return pydantic_field_to_datatype(
+            datatype,
+            model_field,
+            children=children,
+            strict=strict,
+            name=name,
+        )
+    else:
+        return construct_datatype(
+            datatype,
+            children=children,
+            name=name,
+        )
+
+
+def pydantic_field_to_datatype(
+    datatype: Type[DataTypeT],
+    field: FieldInfo,
+    children: Optional[Dict[str, "DataType"]] = None,
+    strict: bool = False,
+    name: Optional[str] = None,
+    **kwargs,
+) -> DataTypeT:
+    if children is None:
+        children = {}
+
+    if not field.json_schema_extra or not isinstance(field.json_schema_extra, dict):
+        validators = []
+    else:
+        validators = field.json_schema_extra.get("validators", [])
+
+    is_optional = not field.is_required()
+
+    if field.title is not None:
+        name = field.title
+    description = field.description
+
+    return construct_datatype(
+        datatype,
+        children,
+        validators,
+        is_optional,
+        name,
+        description,
+        strict=strict,
+        **kwargs,
+    )
+
+
+def construct_datatype(
+    datatype: Type[DataTypeT],
+    children: Optional[Dict[str, Any]] = None,
+    validators: Optional[List[Validator]] = None,
+    optional: bool = False,
+    name: Optional[str] = None,
+    description: Optional[str] = None,
+    strict: bool = False,
+    **kwargs,
+) -> DataTypeT:
+    if children is None:
+        children = {}
+    if validators is None:
+        validators = []
+
+    format_attr = FormatAttr.from_validators(validators, datatype.tag, strict)
+    return datatype(children, format_attr, optional, name, description, **kwargs)
diff --git a/guardrails/validators.py b/guardrails/validators.py
index 7f090bd19..8c3d5844a 100644
--- a/guardrails/validators.py
+++ b/guardrails/validators.py
@@ -13,7 +13,7 @@
 import string
 import warnings
 from functools import partial
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
 
 import openai
 import rstr
@@ -41,7 +41,14 @@
 try:
     import detect_secrets  # type: ignore
 except ImportError:
-    detect_secrets = None  # type: ignore
+    detect_secrets = None
+
+try:
+    from presidio_analyzer import AnalyzerEngine
+    from presidio_anonymizer import AnonymizerEngine
+except ImportError:
+    AnalyzerEngine = None
+    AnonymizerEngine = None
 
 try:
     import nltk  # type: ignore
@@ -2067,6 +2074,130 @@ def query_vector_collection(
         return top_chunks
 
 
+@register_validator(name="pii", data_type="string")
+class PIIFilter(Validator):
+    """Validates that any text does not contain any PII.
+
+    This validator uses Microsoft's Presidio (https://github.com/microsoft/presidio)
+    to detect PII in the text. If PII is detected, the validator will fail with a
+    programmatic fix that anonymizes the text. Otherwise, the validator will pass.
+
+    **Key Properties**
+
+    | Property                      | Description                         |
+    | ----------------------------- | ----------------------------------- |
+    | Name for `format` attribute   | `pii`                               |
+    | Supported data types          | `string`                            |
+    | Programmatic fix              | Anonymized text with PII filtered   |
+
+    Parameters: Arguments
+        pii_entities (str | List[str], optional): The PII entities to filter. Must be
+            one of `pii` or `spi`. Defaults to None. Can also be set in metadata.
+    """
+
+    PII_ENTITIES_MAP = {
+        "pii": [
+            "EMAIL_ADDRESS",
+            "PHONE_NUMBER",
+            "DOMAIN_NAME",
+            "IP_ADDRESS",
+            "DATE_TIME",
+            "LOCATION",
+            "PERSON",
+            "URL",
+        ],
+        "spi": [
+            "CREDIT_CARD",
+            "CRYPTO",
+            "IBAN_CODE",
+            "NRP",
+            "MEDICAL_LICENSE",
+            "US_BANK_NUMBER",
+            "US_DRIVER_LICENSE",
+            "US_ITIN",
+            "US_PASSPORT",
+            "US_SSN",
+        ],
+    }
+
+    def __init__(
+        self,
+        pii_entities: Union[str, List[str], None] = None,
+        on_fail: Union[Callable[..., Any], None] = None,
+        **kwargs,
+    ):
+        if AnalyzerEngine is None or AnonymizerEngine is None:
+            raise ImportError(
+                "You must install the `presidio-analyzer`, `presidio-anonymizer`"
+                "and a spaCy language model to use the PII validator."
+                "Refer to https://microsoft.github.io/presidio/installation/"
+            )
+
+        super().__init__(on_fail, pii_entities=pii_entities, **kwargs)
+        self.pii_entities = pii_entities
+        self.pii_analyzer = AnalyzerEngine()
+        self.pii_anonymizer = AnonymizerEngine()
+
+    def get_anonymized_text(self, text: str, entities: List[str]) -> str:
+        """Analyze and anonymize the text for PII.
+
+        Args:
+            text (str): The text to analyze.
+            pii_entities (List[str]): The PII entities to filter.
+
+        Returns:
+            anonymized_text (str): The anonymized text.
+        """
+        results = self.pii_analyzer.analyze(text=text, entities=entities, language="en")
+        results = cast(List[Any], results)
+        anonymized_text = self.pii_anonymizer.anonymize(
+            text=text, analyzer_results=results
+        ).text
+        return anonymized_text
+
+    def validate(self, value: Any, metadata: Dict[str, Any]) -> ValidationResult:
+        # Entities to filter passed through metadata take precedence
+        pii_entities = metadata.get("pii_entities", self.pii_entities)
+        if pii_entities is None:
+            raise ValueError(
+                "`pii_entities` must be set in order to use the `PIIFilter` validator."
+                "Add this: `pii_entities=['PERSON', 'PHONE_NUMBER']`"
+                "OR pii_entities='pii' or 'spi'"
+                "in init or metadata."
+            )
+
+        # Check that pii_entities is a string OR list of strings
+        if isinstance(pii_entities, str):
+            # A key to the PII_ENTITIES_MAP
+            entities_to_filter = self.PII_ENTITIES_MAP.get(pii_entities, None)
+            if entities_to_filter is None:
+                raise ValueError(
+                    f"`pii_entities` must be one of {self.PII_ENTITIES_MAP.keys()}"
+                )
+        elif isinstance(pii_entities, list):
+            entities_to_filter = pii_entities
+        else:
+            raise ValueError(
+                f"`pii_entities` must be one of {self.PII_ENTITIES_MAP.keys()}"
+                " or a list of strings."
+            )
+
+        # Analyze the text, and anonymize it if there is PII
+        anonymized_text = self.get_anonymized_text(
+            text=value, entities=entities_to_filter
+        )
+
+        # If anonymized value text is different from original value, then there is PII
+        if anonymized_text != value:
+            return FailResult(
+                error_message=(
+                    f"The following text in your response contains PII:\n{value}"
+                ),
+                fix_value=anonymized_text,
+            )
+        return PassResult()
+
+
 @register_validator(name="similar-to-list", data_type="string")
 class SimilarToList(Validator):
     """Validates that a value is similar to a list of previously known values.
diff --git a/poetry.lock b/poetry.lock
index 7542c3363..44ab1224e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
 
 [[package]]
 name = "aiohttp"
@@ -294,6 +294,55 @@ webencodings = "*"
 [package.extras]
 css = ["tinycss2 (>=1.1.0,<1.3)"]
 
+[[package]]
+name = "blis"
+version = "0.7.11"
+description = "The Blis BLAS-like linear algebra library, as a self-contained C-extension."
+optional = true
+python-versions = "*"
+files = [
+    {file = "blis-0.7.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd5fba34c5775e4c440d80e4dea8acb40e2d3855b546e07c4e21fad8f972404c"},
+    {file = "blis-0.7.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:31273d9086cab9c56986d478e3ed6da6752fa4cdd0f7b5e8e5db30827912d90d"},
+    {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06883f83d4c8de8264154f7c4a420b4af323050ed07398c1ff201c34c25c0d2"},
+    {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee493683e3043650d4413d531e79e580d28a3c7bdd184f1b9cfa565497bda1e7"},
+    {file = "blis-0.7.11-cp310-cp310-win_amd64.whl", hash = "sha256:a73945a9d635eea528bccfdfcaa59dd35bd5f82a4a40d5ca31f08f507f3a6f81"},
+    {file = "blis-0.7.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b68df4d01d62f9adaef3dad6f96418787265a6878891fc4e0fabafd6d02afba"},
+    {file = "blis-0.7.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:162e60d941a8151418d558a94ee5547cb1bbeed9f26b3b6f89ec9243f111a201"},
+    {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686a7d0111d5ba727cd62f374748952fd6eb74701b18177f525b16209a253c01"},
+    {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0421d6e44cda202b113a34761f9a062b53f8c2ae8e4ec8325a76e709fca93b6e"},
+    {file = "blis-0.7.11-cp311-cp311-win_amd64.whl", hash = "sha256:0dc9dcb3843045b6b8b00432409fd5ee96b8344a324e031bfec7303838c41a1a"},
+    {file = "blis-0.7.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dadf8713ea51d91444d14ad4104a5493fa7ecc401bbb5f4a203ff6448fadb113"},
+    {file = "blis-0.7.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcdaf370f03adaf4171d6405a89fa66cb3c09399d75fc02e1230a78cd2759e4"},
+    {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7de19264b1d49a178bf8035406d0ae77831f3bfaa3ce02942964a81a202abb03"},
+    {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea55c6a4a60fcbf6a0fdce40df6e254451ce636988323a34b9c94b583fc11e5"},
+    {file = "blis-0.7.11-cp312-cp312-win_amd64.whl", hash = "sha256:5a305dbfc96d202a20d0edd6edf74a406b7e1404f4fa4397d24c68454e60b1b4"},
+    {file = "blis-0.7.11-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:68544a1cbc3564db7ba54d2bf8988356b8c7acd025966e8e9313561b19f0fe2e"},
+    {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075431b13b9dd7b411894d4afbd4212acf4d0f56c5a20628f4b34902e90225f1"},
+    {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:324fdf62af9075831aa62b51481960e8465674b7723f977684e32af708bb7448"},
+    {file = "blis-0.7.11-cp36-cp36m-win_amd64.whl", hash = "sha256:afebdb02d2dcf9059f23ce1244585d3ce7e95c02a77fd45a500e4a55b7b23583"},
+    {file = "blis-0.7.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2e62cd14b20e960f21547fee01f3a0b2ac201034d819842865a667c969c355d1"},
+    {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b01c05a5754edc0b9a3b69be52cbee03f645b2ec69651d12216ea83b8122f0"},
+    {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfee5ec52ba1e9002311d9191f7129d7b0ecdff211e88536fb24c865d102b50d"},
+    {file = "blis-0.7.11-cp37-cp37m-win_amd64.whl", hash = "sha256:844b6377e3e7f3a2e92e7333cc644095386548ad5a027fdc150122703c009956"},
+    {file = "blis-0.7.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6df00c24128e323174cde5d80ebe3657df39615322098ce06613845433057614"},
+    {file = "blis-0.7.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:809d1da1331108935bf06e22f3cf07ef73a41a572ecd81575bdedb67defe3465"},
+    {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfabd5272bbbe504702b8dfe30093653d278057656126716ff500d9c184b35a6"},
+    {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca684f5c2f05269f17aefe7812360286e9a1cee3afb96d416485efd825dbcf19"},
+    {file = "blis-0.7.11-cp38-cp38-win_amd64.whl", hash = "sha256:688a8b21d2521c2124ee8dfcbaf2c385981ccc27e313e052113d5db113e27d3b"},
+    {file = "blis-0.7.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2ff7abd784033836b284ff9f4d0d7cb0737b7684daebb01a4c9fe145ffa5a31e"},
+    {file = "blis-0.7.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9caffcd14795bfe52add95a0dd8426d44e737b55fcb69e2b797816f4da0b1d2"},
+    {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fb36989ed61233cfd48915896802ee6d3d87882190000f8cfe0cf4a3819f9a8"},
+    {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ea09f961871f880d5dc622dce6c370e4859559f0ead897ae9b20ddafd6b07a2"},
+    {file = "blis-0.7.11-cp39-cp39-win_amd64.whl", hash = "sha256:5bb38adabbb22f69f22c74bad025a010ae3b14de711bf5c715353980869d491d"},
+    {file = "blis-0.7.11.tar.gz", hash = "sha256:cec6d48f75f7ac328ae1b6fbb372dde8c8a57c89559172277f66e01ff08d4d42"},
+]
+
+[package.dependencies]
+numpy = [
+    {version = ">=1.15.0", markers = "python_version < \"3.9\""},
+    {version = ">=1.19.0", markers = "python_version >= \"3.9\""},
+]
+
 [[package]]
 name = "boltons"
 version = "23.1.1"
@@ -346,6 +395,17 @@ tinycss2 = "*"
 doc = ["sphinx", "sphinx-rtd-theme"]
 test = ["flake8", "isort", "pytest"]
 
+[[package]]
+name = "catalogue"
+version = "2.0.10"
+description = "Super lightweight function registries for your library"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f"},
+    {file = "catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15"},
+]
+
 [[package]]
 name = "certifi"
 version = "2023.7.22"
@@ -545,6 +605,26 @@ files = [
 [package.dependencies]
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
 
+[[package]]
+name = "cloudpathlib"
+version = "0.16.0"
+description = "pathlib-style classes for cloud storage services."
+optional = true
+python-versions = ">=3.7"
+files = [
+    {file = "cloudpathlib-0.16.0-py3-none-any.whl", hash = "sha256:f46267556bf91f03db52b5df7a152548596a15aabca1c8731ef32b0b25a1a6a3"},
+    {file = "cloudpathlib-0.16.0.tar.gz", hash = "sha256:cdfcd35d46d529587d744154a0bdf962aca953b725c8784cd2ec478354ea63a3"},
+]
+
+[package.dependencies]
+typing_extensions = {version = ">4", markers = "python_version < \"3.11\""}
+
+[package.extras]
+all = ["cloudpathlib[azure]", "cloudpathlib[gs]", "cloudpathlib[s3]"]
+azure = ["azure-storage-blob (>=12)"]
+gs = ["google-cloud-storage"]
+s3 = ["boto3"]
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -583,6 +663,21 @@ traitlets = ">=4"
 [package.extras]
 test = ["pytest"]
 
+[[package]]
+name = "confection"
+version = "0.1.3"
+description = "The sweetest config system for Python"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "confection-0.1.3-py3-none-any.whl", hash = "sha256:58b125c9bc6786f32e37fe4d98bc3a03e5f509a4b9de02541b99c559f2026092"},
+    {file = "confection-0.1.3.tar.gz", hash = "sha256:5a876d368a7698eec58791126757a75a3df16e26cc49653b52426e9ffd39f12f"},
+]
+
+[package.dependencies]
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0"
+srsly = ">=2.4.0,<3.0.0"
+
 [[package]]
 name = "coverage"
 version = "7.3.2"
@@ -714,6 +809,48 @@ webencodings = "*"
 doc = ["sphinx", "sphinx_rtd_theme"]
 test = ["flake8", "isort", "pytest"]
 
+[[package]]
+name = "cymem"
+version = "2.0.8"
+description = "Manage calls to calloc/free through Cython"
+optional = true
+python-versions = "*"
+files = [
+    {file = "cymem-2.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77b5d3a73c41a394efd5913ab7e48512054cd2dabb9582d489535456641c7666"},
+    {file = "cymem-2.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd33da892fb560ba85ea14b1528c381ff474048e861accc3366c8b491035a378"},
+    {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a551eda23eebd6d076b855f77a5ed14a1d1cae5946f7b3cb5de502e21b39b0"},
+    {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8260445652ae5ab19fff6851f32969a7b774f309162e83367dd0f69aac5dbf7"},
+    {file = "cymem-2.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:a63a2bef4c7e0aec7c9908bca0a503bf91ac7ec18d41dd50dc7dff5d994e4387"},
+    {file = "cymem-2.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b84b780d52cb2db53d4494fe0083c4c5ee1f7b5380ceaea5b824569009ee5bd"},
+    {file = "cymem-2.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d5f83dc3cb5a39f0e32653cceb7c8ce0183d82f1162ca418356f4a8ed9e203e"},
+    {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac218cf8a43a761dc6b2f14ae8d183aca2bbb85b60fe316fd6613693b2a7914"},
+    {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c993589d1811ec665d37437d5677b8757f53afadd927bf8516ac8ce2d3a50c"},
+    {file = "cymem-2.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:ab3cf20e0eabee9b6025ceb0245dadd534a96710d43fb7a91a35e0b9e672ee44"},
+    {file = "cymem-2.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cb51fddf1b920abb1f2742d1d385469bc7b4b8083e1cfa60255e19bc0900ccb5"},
+    {file = "cymem-2.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9235957f8c6bc2574a6a506a1687164ad629d0b4451ded89d49ebfc61b52660c"},
+    {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2cc38930ff5409f8d61f69a01e39ecb185c175785a1c9bec13bcd3ac8a614ba"},
+    {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bf49e3ea2c441f7b7848d5c61b50803e8cbd49541a70bb41ad22fce76d87603"},
+    {file = "cymem-2.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:ecd12e3bacf3eed5486e4cd8ede3c12da66ee0e0a9d0ae046962bc2bb503acef"},
+    {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:167d8019db3b40308aabf8183fd3fbbc256323b645e0cbf2035301058c439cd0"},
+    {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cd2c2791c8f6b52f269a756ba7463f75bf7265785388a2592623b84bb02bf8"},
+    {file = "cymem-2.0.8-cp36-cp36m-win_amd64.whl", hash = "sha256:6204f0a3307bf45d109bf698ba37997ce765f21e359284328e4306c7500fcde8"},
+    {file = "cymem-2.0.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9c05db55ea338648f8e5f51dd596568c7f62c5ae32bf3fa5b1460117910ebae"},
+    {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ce641f7ba0489bd1b42a4335a36f38c8507daffc29a512681afaba94a0257d2"},
+    {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b83a5972a64f62796118da79dfeed71f4e1e770b2b7455e889c909504c2358"},
+    {file = "cymem-2.0.8-cp37-cp37m-win_amd64.whl", hash = "sha256:ada6eb022e4a0f4f11e6356a5d804ceaa917174e6cf33c0b3e371dbea4dd2601"},
+    {file = "cymem-2.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e593cd57e2e19eb50c7ddaf7e230b73c890227834425b9dadcd4a86834ef2ab"},
+    {file = "cymem-2.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d513f0d5c6d76facdc605e42aa42c8d50bb7dedca3144ec2b47526381764deb0"},
+    {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e370dd54359101b125bfb191aca0542718077b4edb90ccccba1a28116640fed"},
+    {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f8c58cde71b8fc7024883031a4eec66c0a9a4d36b7850c3065493652695156"},
+    {file = "cymem-2.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:6a6edddb30dd000a27987fcbc6f3c23b7fe1d74f539656952cb086288c0e4e29"},
+    {file = "cymem-2.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b896c83c08dadafe8102a521f83b7369a9c5cc3e7768eca35875764f56703f4c"},
+    {file = "cymem-2.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f8f2bfee34f6f38b206997727d29976666c89843c071a968add7d61a1e8024"},
+    {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7372e2820fa66fd47d3b135f3eb574ab015f90780c3a21cfd4809b54f23a4723"},
+    {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e57bee56d35b90fc2cba93e75b2ce76feaca05251936e28a96cf812a1f5dda"},
+    {file = "cymem-2.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ceeab3ce2a92c7f3b2d90854efb32cb203e78cb24c836a5a9a2cac221930303b"},
+    {file = "cymem-2.0.8.tar.gz", hash = "sha256:8fb09d222e21dcf1c7e907dc85cf74501d4cea6c4ed4ac6c9e016f98fb59cbbf"},
+]
+
 [[package]]
 name = "debugpy"
 version = "1.8.0"
@@ -1596,6 +1733,20 @@ completion = ["shtab"]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
+[[package]]
+name = "langcodes"
+version = "3.3.0"
+description = "Tools for labeling human languages with IETF language tags"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "langcodes-3.3.0-py3-none-any.whl", hash = "sha256:4d89fc9acb6e9c8fdef70bcdf376113a3db09b67285d9e1d534de6d8818e7e69"},
+    {file = "langcodes-3.3.0.tar.gz", hash = "sha256:794d07d5a28781231ac335a1561b8442f8648ca07cd518310aeb45d6f0807ef6"},
+]
+
+[package.extras]
+data = ["language-data (>=1.1,<2.0)"]
+
 [[package]]
 name = "lxml"
 version = "4.9.3"
@@ -2207,6 +2358,48 @@ files = [
     {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
 ]
 
+[[package]]
+name = "murmurhash"
+version = "1.0.10"
+description = "Cython bindings for MurmurHash"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "murmurhash-1.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e90eef568adca5e17a91f96975e9a782ace3a617bbb3f8c8c2d917096e9bfeb"},
+    {file = "murmurhash-1.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f8ecb00cc1ab57e4b065f9fb3ea923b55160c402d959c69a0b6dbbe8bc73efc3"},
+    {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3310101004d9e2e0530c2fed30174448d998ffd1b50dcbfb7677e95db101aa4b"},
+    {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65401a6f1778676253cbf89c1f45a8a7feb7d73038e483925df7d5943c08ed9"},
+    {file = "murmurhash-1.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:f23f2dfc7174de2cdc5007c0771ab8376a2a3f48247f32cac4a5563e40c6adcc"},
+    {file = "murmurhash-1.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90ed37ee2cace9381b83d56068334f77e3e30bc521169a1f886a2a2800e965d6"},
+    {file = "murmurhash-1.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22e9926fdbec9d24ced9b0a42f0fee68c730438be3cfb00c2499fd495caec226"},
+    {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bfbfd68baa99717239b8844600db627f336a08b1caf4df89762999f681cdd1"},
+    {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b9d200a09d48ef67f6840b77c14f151f2b6c48fd69661eb75c7276ebdb146c"},
+    {file = "murmurhash-1.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:e5d7cfe392c0a28129226271008e61e77bf307afc24abf34f386771daa7b28b0"},
+    {file = "murmurhash-1.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:96f0a070344d4802ea76a160e0d4c88b7dc10454d2426f48814482ba60b38b9e"},
+    {file = "murmurhash-1.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f61862060d677c84556610ac0300a0776cb13cb3155f5075ed97e80f86e55d9"},
+    {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3b6d2d877d8881a08be66d906856d05944be0faf22b9a0390338bcf45299989"},
+    {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f54b0031d8696fed17ed6e9628f339cdea0ba2367ca051e18ff59193f52687"},
+    {file = "murmurhash-1.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:97e09d675de2359e586f09de1d0de1ab39f9911edffc65c9255fb5e04f7c1f85"},
+    {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b64e5332932993fef598e78d633b1ba664789ab73032ed511f3dc615a631a1a"},
+    {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2a38437a8497e082408aa015c6d90554b9e00c2c221fdfa79728a2d99a739e"},
+    {file = "murmurhash-1.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:55f4e4f9291a53c36070330950b472d72ba7d331e4ce3ce1ab349a4f458f7bc4"},
+    {file = "murmurhash-1.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:16ef9f0855952493fe08929d23865425906a8c0c40607ac8a949a378652ba6a9"},
+    {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cc3351ae92b89c2fcdc6e41ac6f17176dbd9b3554c96109fd0713695d8663e7"},
+    {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6559fef7c2e7349a42a63549067709b656d6d1580752bd76be1541d8b2d65718"},
+    {file = "murmurhash-1.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:8bf49e3bb33febb7057ae3a5d284ef81243a1e55eaa62bdcd79007cddbdc0461"},
+    {file = "murmurhash-1.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f1605fde07030516eb63d77a598dd164fb9bf217fd937dbac588fe7e47a28c40"},
+    {file = "murmurhash-1.0.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4904f7e68674a64eb2b08823c72015a5e14653e0b4b109ea00c652a005a59bad"},
+    {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0438f0cb44cf1cd26251f72c1428213c4197d40a4e3f48b1efc3aea12ce18517"},
+    {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1171a3f9a10571931764cdbfaa5371f4cf5c23c680639762125cb075b833a5"},
+    {file = "murmurhash-1.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:1c9fbcd7646ad8ba67b895f71d361d232c6765754370ecea473dd97d77afe99f"},
+    {file = "murmurhash-1.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7024ab3498434f22f8e642ae31448322ad8228c65c8d9e5dc2d563d57c14c9b8"},
+    {file = "murmurhash-1.0.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a99dedfb7f0cc5a4cd76eb409ee98d3d50eba024f934e705914f6f4d765aef2c"},
+    {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b580b8503647de5dd7972746b7613ea586270f17ac92a44872a9b1b52c36d68"},
+    {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75840212bf75eb1352c946c3cf1622dacddd6d6bdda34368237d1eb3568f23a"},
+    {file = "murmurhash-1.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:a4209962b9f85de397c3203ea4b3a554da01ae9fd220fdab38757d4e9eba8d1a"},
+    {file = "murmurhash-1.0.10.tar.gz", hash = "sha256:5282aab1317804c6ebd6dd7f69f15ba9075aee671c44a34be2bde0f1b11ef88a"},
+]
+
 [[package]]
 name = "mypy-extensions"
 version = "1.0.0"
@@ -2564,6 +2757,17 @@ files = [
 [package.dependencies]
 ptyprocess = ">=0.5"
 
+[[package]]
+name = "phonenumbers"
+version = "8.13.25"
+description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
+optional = true
+python-versions = "*"
+files = [
+    {file = "phonenumbers-8.13.25-py2.py3-none-any.whl", hash = "sha256:7a57cceb8145d3099a0cda7a1f2581b6829936069224790be5de0adf14b39f13"},
+    {file = "phonenumbers-8.13.25.tar.gz", hash = "sha256:4ae2d2e253a4752a269ae1147822b9aa500f14b2506a91f884e68b136901f128"},
+]
+
 [[package]]
 name = "pickleshare"
 version = "0.7.5"
@@ -2715,6 +2919,86 @@ nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 virtualenv = ">=20.10.0"
 
+[[package]]
+name = "preshed"
+version = "3.0.9"
+description = "Cython hash table that trusts the keys are pre-hashed"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "preshed-3.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f96ef4caf9847b2bb9868574dcbe2496f974e41c2b83d6621c24fb4c3fc57e3"},
+    {file = "preshed-3.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a61302cf8bd30568631adcdaf9e6b21d40491bd89ba8ebf67324f98b6c2a2c05"},
+    {file = "preshed-3.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99499e8a58f58949d3f591295a97bca4e197066049c96f5d34944dd21a497193"},
+    {file = "preshed-3.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea6b6566997dc3acd8c6ee11a89539ac85c77275b4dcefb2dc746d11053a5af8"},
+    {file = "preshed-3.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:bfd523085a84b1338ff18f61538e1cfcdedc4b9e76002589a301c364d19a2e36"},
+    {file = "preshed-3.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7c2364da27f2875524ce1ca754dc071515a9ad26eb5def4c7e69129a13c9a59"},
+    {file = "preshed-3.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182138033c0730c683a6d97e567ceb8a3e83f3bff5704f300d582238dbd384b3"},
+    {file = "preshed-3.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345a10be3b86bcc6c0591d343a6dc2bfd86aa6838c30ced4256dfcfa836c3a64"},
+    {file = "preshed-3.0.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51d0192274aa061699b284f9fd08416065348edbafd64840c3889617ee1609de"},
+    {file = "preshed-3.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:96b857d7a62cbccc3845ac8c41fd23addf052821be4eb987f2eb0da3d8745aa1"},
+    {file = "preshed-3.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4fe6720012c62e6d550d6a5c1c7ad88cacef8388d186dad4bafea4140d9d198"},
+    {file = "preshed-3.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e04f05758875be9751e483bd3c519c22b00d3b07f5a64441ec328bb9e3c03700"},
+    {file = "preshed-3.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a55091d0e395f1fdb62ab43401bb9f8b46c7d7794d5b071813c29dc1ab22fd0"},
+    {file = "preshed-3.0.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7de8f5138bcac7870424e09684dc3dd33c8e30e81b269f6c9ede3d8c7bb8e257"},
+    {file = "preshed-3.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:24229c77364628743bc29c5620c5d6607ed104f0e02ae31f8a030f99a78a5ceb"},
+    {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73b0f7ecc58095ebbc6ca26ec806008ef780190fe685ce471b550e7eef58dc2"},
+    {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb90ecd5bec71c21d95962db1a7922364d6db2abe284a8c4b196df8bbcc871e"},
+    {file = "preshed-3.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:e304a0a8c9d625b70ba850c59d4e67082a6be9c16c4517b97850a17a282ebee6"},
+    {file = "preshed-3.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1fa6d3d5529b08296ff9b7b4da1485c080311fd8744bbf3a86019ff88007b382"},
+    {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e5173809d85edd420fc79563b286b88b4049746b797845ba672cf9435c0e7"},
+    {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fe81eb21c7d99e8b9a802cc313b998c5f791bda592903c732b607f78a6b7dc4"},
+    {file = "preshed-3.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:78590a4a952747c3766e605ce8b747741005bdb1a5aa691a18aae67b09ece0e6"},
+    {file = "preshed-3.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3452b64d97ce630e200c415073040aa494ceec6b7038f7a2a3400cbd7858e952"},
+    {file = "preshed-3.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ac970d97b905e9e817ec13d31befd5b07c9cfec046de73b551d11a6375834b79"},
+    {file = "preshed-3.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eebaa96ece6641cd981491cba995b68c249e0b6877c84af74971eacf8990aa19"},
+    {file = "preshed-3.0.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d473c5f6856e07a88d41fe00bb6c206ecf7b34c381d30de0b818ba2ebaf9406"},
+    {file = "preshed-3.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:0de63a560f10107a3f0a9e252cc3183b8fdedcb5f81a86938fd9f1dcf8a64adf"},
+    {file = "preshed-3.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a9ad9f738084e048a7c94c90f40f727217387115b2c9a95c77f0ce943879fcd"},
+    {file = "preshed-3.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a671dfa30b67baa09391faf90408b69c8a9a7f81cb9d83d16c39a182355fbfce"},
+    {file = "preshed-3.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23906d114fc97c17c5f8433342495d7562e96ecfd871289c2bb2ed9a9df57c3f"},
+    {file = "preshed-3.0.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778cf71f82cedd2719b256f3980d556d6fb56ec552334ba79b49d16e26e854a0"},
+    {file = "preshed-3.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:a6e579439b329eb93f32219ff27cb358b55fbb52a4862c31a915a098c8a22ac2"},
+    {file = "preshed-3.0.9.tar.gz", hash = "sha256:721863c5244ffcd2651ad0928951a2c7c77b102f4e11a251ad85d37ee7621660"},
+]
+
+[package.dependencies]
+cymem = ">=2.0.2,<2.1.0"
+murmurhash = ">=0.28.0,<1.1.0"
+
+[[package]]
+name = "presidio-analyzer"
+version = "2.2.351"
+description = "Presidio analyzer package"
+optional = true
+python-versions = "*"
+files = [
+    {file = "presidio_analyzer-2.2.351-py3-none-any.whl", hash = "sha256:9e4144ca4a809b084acf273d7e9615182f50e2bd6c195fe83d585fd24b507876"},
+]
+
+[package.dependencies]
+phonenumbers = ">=8.12,<9.0.0"
+pyyaml = "*"
+regex = "*"
+spacy = ">=3.4.4,<4.0.0"
+tldextract = "*"
+
+[package.extras]
+stanza = ["spacy-stanza", "stanza"]
+transformers = ["spacy-huggingface-pipelines"]
+
+[[package]]
+name = "presidio-anonymizer"
+version = "2.2.351"
+description = "Persidio Anonymizer package - replaces analyzed text with desired values."
+optional = true
+python-versions = ">=3.5"
+files = [
+    {file = "presidio_anonymizer-2.2.351-py3-none-any.whl", hash = "sha256:a898c38afcf01ac21aa7558702b8f22e378625854574df0b277c553bb3164f50"},
+]
+
+[package.dependencies]
+pycryptodome = ">=3.10.1"
+
 [[package]]
 name = "prompt-toolkit"
 version = "3.0.39"
@@ -2804,6 +3088,47 @@ files = [
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
 ]
 
+[[package]]
+name = "pycryptodome"
+version = "3.19.0"
+description = "Cryptographic library for Python"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+    {file = "pycryptodome-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3006c44c4946583b6de24fe0632091c2653d6256b99a02a3db71ca06472ea1e4"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c760c8a0479a4042111a8dd2f067d3ae4573da286c53f13cf6f5c53a5c1f631"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:08ce3558af5106c632baf6d331d261f02367a6bc3733086ae43c0f988fe042db"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45430dfaf1f421cf462c0dd824984378bef32b22669f2635cb809357dbaab405"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a9bcd5f3794879e91970f2bbd7d899780541d3ff439d8f2112441769c9f2ccea"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:190c53f51e988dceb60472baddce3f289fa52b0ec38fbe5fd20dd1d0f795c551"},
+    {file = "pycryptodome-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:22e0ae7c3a7f87dcdcf302db06ab76f20e83f09a6993c160b248d58274473bfa"},
+    {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7822f36d683f9ad7bc2145b2c2045014afdbbd1d9922a6d4ce1cbd6add79a01e"},
+    {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:05e33267394aad6db6595c0ce9d427fe21552f5425e116a925455e099fdf759a"},
+    {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829b813b8ee00d9c8aba417621b94bc0b5efd18c928923802ad5ba4cf1ec709c"},
+    {file = "pycryptodome-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:fc7a79590e2b5d08530175823a242de6790abc73638cc6dc9d2684e7be2f5e49"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:542f99d5026ac5f0ef391ba0602f3d11beef8e65aae135fa5b762f5ebd9d3bfb"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61bb3ccbf4bf32ad9af32da8badc24e888ae5231c617947e0f5401077f8b091f"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d49a6c715d8cceffedabb6adb7e0cbf41ae1a2ff4adaeec9432074a80627dea1"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e249a784cc98a29c77cea9df54284a44b40cafbfae57636dd2f8775b48af2434"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d033947e7fd3e2ba9a031cb2d267251620964705a013c5a461fa5233cc025270"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:84c3e4fffad0c4988aef0d5591be3cad4e10aa7db264c65fadbc633318d20bde"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:139ae2c6161b9dd5d829c9645d781509a810ef50ea8b657e2257c25ca20efe33"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5b1986c761258a5b4332a7f94a83f631c1ffca8747d75ab8395bf2e1b93283d9"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-win32.whl", hash = "sha256:536f676963662603f1f2e6ab01080c54d8cd20f34ec333dcb195306fa7826997"},
+    {file = "pycryptodome-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:04dd31d3b33a6b22ac4d432b3274588917dcf850cc0c51c84eca1d8ed6933810"},
+    {file = "pycryptodome-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:8999316e57abcbd8085c91bc0ef75292c8618f41ca6d2b6132250a863a77d1e7"},
+    {file = "pycryptodome-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:a0ab84755f4539db086db9ba9e9f3868d2e3610a3948cbd2a55e332ad83b01b0"},
+    {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0101f647d11a1aae5a8ce4f5fad6644ae1b22bb65d05accc7d322943c69a74a6"},
+    {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1601e04d32087591d78e0b81e1e520e57a92796089864b20e5f18c9564b3fa"},
+    {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506c686a1eee6c00df70010be3b8e9e78f406af4f21b23162bbb6e9bdf5427bc"},
+    {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7919ccd096584b911f2a303c593280869ce1af9bf5d36214511f5e5a1bed8c34"},
+    {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:560591c0777f74a5da86718f70dfc8d781734cf559773b64072bbdda44b3fc3e"},
+    {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cc2f2ae451a676def1a73c1ae9120cd31af25db3f381893d45f75e77be2400"},
+    {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17940dcf274fcae4a54ec6117a9ecfe52907ed5e2e438fe712fe7ca502672ed5"},
+    {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d04f5f623a280fbd0ab1c1d8ecbd753193ab7154f09b6161b0f857a1a676c15f"},
+    {file = "pycryptodome-3.19.0.tar.gz", hash = "sha256:bc35d463222cdb4dbebd35e0784155c81e161b9284e567e7e933d722e533331e"},
+]
+
 [[package]]
 name = "pydantic"
 version = "1.10.9"
@@ -3564,6 +3889,21 @@ urllib3 = ">=1.21.1,<3"
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
+[[package]]
+name = "requests-file"
+version = "1.5.1"
+description = "File transport adapter for Requests"
+optional = true
+python-versions = "*"
+files = [
+    {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"},
+    {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"},
+]
+
+[package.dependencies]
+requests = ">=1.0.0"
+six = "*"
+
 [[package]]
 name = "requests-toolbelt"
 version = "1.0.0"
@@ -3857,6 +4197,27 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
+[[package]]
+name = "smart-open"
+version = "6.4.0"
+description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)"
+optional = true
+python-versions = ">=3.6,<4.0"
+files = [
+    {file = "smart_open-6.4.0-py3-none-any.whl", hash = "sha256:8d3ef7e6997e8e42dd55c74166ed21e6ac70664caa32dd940b26d54a8f6b4142"},
+    {file = "smart_open-6.4.0.tar.gz", hash = "sha256:be3c92c246fbe80ebce8fbacb180494a481a77fcdcb7c1aadb2ea5b9c2bee8b9"},
+]
+
+[package.extras]
+all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests"]
+azure = ["azure-common", "azure-core", "azure-storage-blob"]
+gcs = ["google-cloud-storage (>=2.6.0)"]
+http = ["requests"]
+s3 = ["boto3"]
+ssh = ["paramiko"]
+test = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "paramiko", "pytest", "pytest-rerunfailures", "requests", "responses"]
+webhdfs = ["requests"]
+
 [[package]]
 name = "smmap"
 version = "5.0.1"
@@ -3879,6 +4240,119 @@ files = [
     {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
 ]
 
+[[package]]
+name = "spacy"
+version = "3.7.2"
+description = "Industrial-strength Natural Language Processing (NLP) in Python"
+optional = true
+python-versions = ">=3.7"
+files = [
+    {file = "spacy-3.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e285366d36c85f784d606a2d966912a18f4d24d47330c1c6acbdd9f19ee373"},
+    {file = "spacy-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f132c05368781be5d3be3d706afce7e7a9a0c9edc0dbb7c616162c37bc386561"},
+    {file = "spacy-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3767b2cabbe337d62779ae4fdc4d57a39755c17dfc499de3ad2bae622caa43"},
+    {file = "spacy-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a748ade269bdbea9baaa49ec00882404e7e921163cdc14f5612320d0a957dfd"},
+    {file = "spacy-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:66467128e494bfa4dc9c3996e4cbb26bac4741bca4cdd8dd83a6e71182148945"},
+    {file = "spacy-3.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5af30aea578e7414fb0eb4dbad0ff0fa0a7d8e833c3e733eceb2617534714c7d"},
+    {file = "spacy-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7293de33b1e9ede151555070ad0fee3bac98aefcaac9e615eeeb4296846bd479"},
+    {file = "spacy-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26940681cf20c8831c558e2c3d345ff20b5bc3c5e6d41c66172d0c5136042f0b"},
+    {file = "spacy-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a334667625153f7aaf188c20af7e82c886e41a88483a056accba5a7d51095c6"},
+    {file = "spacy-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:43e6147d3583b62a2d3af0cd913ac025068196d587345751e198391ff0b8c1e9"},
+    {file = "spacy-3.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2558df8c11905a0f77a2a3639a12ef8a522d171bcd88eaec039bedf6c60d7e01"},
+    {file = "spacy-3.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df1b9c4bbadc89bad10dba226d52c113e231ea6ad35c8a916ab138b31f69fa24"},
+    {file = "spacy-3.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbe055d2170ac7505a9f580bbdcd2146d0701bdbd6cea2333e18b0db655b97a"},
+    {file = "spacy-3.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d35129b16ae2ca4212bf22a5c88b67b1e019e434fc48b69d3b95f80bc9e14e42"},
+    {file = "spacy-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:a7419682aba99624cc4df7df66764b6ec62ff415f32c3682c1af2a37bd11a913"},
+    {file = "spacy-3.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b12ab9c4923ffd38da84baf09464982da44e8275d680fb3c5da2051d7dd7bd2d"},
+    {file = "spacy-3.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c5c9db529dc1caa908813c58ba1643e929d2c811768596a2b64e2e01a882b1"},
+    {file = "spacy-3.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcaad95e3e7d0ea8f381f3e2d9e80b7f346ecb6566de9bd55361736fa563fc22"},
+    {file = "spacy-3.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9b12284871ca5daa7774604a964486957567a86f1af898da0260e94b815e0d"},
+    {file = "spacy-3.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2bd89770f61d5980e788ef382297322cceb7dcc4b848d68cb1da8af7d80d6eb6"},
+    {file = "spacy-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d42f9151a2f01b34227ed31c8db8b7c67889ebcc637eae390faec8093ea1fb12"},
+    {file = "spacy-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3d25d2f22ba1d2dd46d103e4a54826582de2b853b6f95dfb97b005563b38838"},
+    {file = "spacy-3.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:730f23340dd157817d2da6df21f69966791b0bdbd6ea108845a65f3e1c0e981c"},
+    {file = "spacy-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:9c2f3f04b4b894a6c42ee93cec2f2b158f246f344927e65d9d19b72c5a6493ea"},
+    {file = "spacy-3.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b22e0e8dac76740d55556fa13ebb9e1c829779ea0b7ec7a9e04f32efc66f74b9"},
+    {file = "spacy-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ad7f378350104ca1f9e81180485d8b094aad7acb9b4bce84f1387b905cf230a2"},
+    {file = "spacy-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ccbffb7825c08c0586ef7384d0aa23196f9ac106b5c7b3c551907316930f94f"},
+    {file = "spacy-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:111955d7f4786b952672e9c5cfd9f8b74d81e64b62d479f71efe9cfc2a027a1d"},
+    {file = "spacy-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:e8a7291e7e1cfcb6041b26f96d0a66b603725c1beff4e0391c3d9226fae16e04"},
+    {file = "spacy-3.7.2.tar.gz", hash = "sha256:cedf4927bf0d3fec773a6ce48d5d2c91bdb02fed3c7d5ec07bdb873f1126f1a0"},
+]
+
+[package.dependencies]
+catalogue = ">=2.0.6,<2.1.0"
+cymem = ">=2.0.2,<2.1.0"
+jinja2 = "*"
+langcodes = ">=3.2.0,<4.0.0"
+murmurhash = ">=0.28.0,<1.1.0"
+numpy = [
+    {version = ">=1.15.0", markers = "python_version < \"3.9\""},
+    {version = ">=1.19.0", markers = "python_version >= \"3.9\""},
+]
+packaging = ">=20.0"
+preshed = ">=3.0.2,<3.1.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0"
+requests = ">=2.13.0,<3.0.0"
+setuptools = "*"
+smart-open = ">=5.2.1,<7.0.0"
+spacy-legacy = ">=3.0.11,<3.1.0"
+spacy-loggers = ">=1.0.0,<2.0.0"
+srsly = ">=2.4.3,<3.0.0"
+thinc = ">=8.1.8,<8.3.0"
+tqdm = ">=4.38.0,<5.0.0"
+typer = ">=0.3.0,<0.10.0"
+wasabi = ">=0.9.1,<1.2.0"
+weasel = ">=0.1.0,<0.4.0"
+
+[package.extras]
+apple = ["thinc-apple-ops (>=0.1.0.dev0,<1.0.0)"]
+cuda = ["cupy (>=5.0.0b4,<13.0.0)"]
+cuda-autodetect = ["cupy-wheel (>=11.0.0,<13.0.0)"]
+cuda100 = ["cupy-cuda100 (>=5.0.0b4,<13.0.0)"]
+cuda101 = ["cupy-cuda101 (>=5.0.0b4,<13.0.0)"]
+cuda102 = ["cupy-cuda102 (>=5.0.0b4,<13.0.0)"]
+cuda110 = ["cupy-cuda110 (>=5.0.0b4,<13.0.0)"]
+cuda111 = ["cupy-cuda111 (>=5.0.0b4,<13.0.0)"]
+cuda112 = ["cupy-cuda112 (>=5.0.0b4,<13.0.0)"]
+cuda113 = ["cupy-cuda113 (>=5.0.0b4,<13.0.0)"]
+cuda114 = ["cupy-cuda114 (>=5.0.0b4,<13.0.0)"]
+cuda115 = ["cupy-cuda115 (>=5.0.0b4,<13.0.0)"]
+cuda116 = ["cupy-cuda116 (>=5.0.0b4,<13.0.0)"]
+cuda117 = ["cupy-cuda117 (>=5.0.0b4,<13.0.0)"]
+cuda11x = ["cupy-cuda11x (>=11.0.0,<13.0.0)"]
+cuda12x = ["cupy-cuda12x (>=11.5.0,<13.0.0)"]
+cuda80 = ["cupy-cuda80 (>=5.0.0b4,<13.0.0)"]
+cuda90 = ["cupy-cuda90 (>=5.0.0b4,<13.0.0)"]
+cuda91 = ["cupy-cuda91 (>=5.0.0b4,<13.0.0)"]
+cuda92 = ["cupy-cuda92 (>=5.0.0b4,<13.0.0)"]
+ja = ["sudachidict-core (>=20211220)", "sudachipy (>=0.5.2,!=0.6.1)"]
+ko = ["natto-py (>=0.9.0)"]
+lookups = ["spacy-lookups-data (>=1.0.3,<1.1.0)"]
+th = ["pythainlp (>=2.0)"]
+transformers = ["spacy-transformers (>=1.1.2,<1.4.0)"]
+
+[[package]]
+name = "spacy-legacy"
+version = "3.0.12"
+description = "Legacy registered functions for spaCy backwards compatibility"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774"},
+    {file = "spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f"},
+]
+
+[[package]]
+name = "spacy-loggers"
+version = "1.0.5"
+description = "Logging utilities for SpaCy"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24"},
+    {file = "spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645"},
+]
+
 [[package]]
 name = "sqlalchemy"
 version = "2.0.23"
@@ -4004,6 +4478,52 @@ files = [
 [package.extras]
 test = ["pytest (==7.2.0)"]
 
+[[package]]
+name = "srsly"
+version = "2.4.8"
+description = "Modern high-performance serialization utilities for Python"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "srsly-2.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:17f3bcb418bb4cf443ed3d4dcb210e491bd9c1b7b0185e6ab10b6af3271e63b2"},
+    {file = "srsly-2.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b070a58e21ab0e878fd949f932385abb4c53dd0acb6d3a7ee75d95d447bc609"},
+    {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98286d20014ed2067ad02b0be1e17c7e522255b188346e79ff266af51a54eb33"},
+    {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18685084e2e0cc47c25158cbbf3e44690e494ef77d6418c2aae0598c893f35b0"},
+    {file = "srsly-2.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:980a179cbf4eb5bc56f7507e53f76720d031bcf0cef52cd53c815720eb2fc30c"},
+    {file = "srsly-2.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5472ed9f581e10c32e79424c996cf54c46c42237759f4224806a0cd4bb770993"},
+    {file = "srsly-2.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50f10afe9230072c5aad9f6636115ea99b32c102f4c61e8236d8642c73ec7a13"},
+    {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c994a89ba247a4d4f63ef9fdefb93aa3e1f98740e4800d5351ebd56992ac75e3"},
+    {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7ed4a0c20fa54d90032be32f9c656b6d75445168da78d14fe9080a0c208ad"},
+    {file = "srsly-2.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:7a919236a090fb93081fbd1cec030f675910f3863825b34a9afbcae71f643127"},
+    {file = "srsly-2.4.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7583c03d114b4478b7a357a1915305163e9eac2dfe080da900555c975cca2a11"},
+    {file = "srsly-2.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ccdd2f6db824c31266aaf93e0f31c1c43b8bc531cd2b3a1d924e3c26a4f294"},
+    {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72d2974f91aee652d606c7def98744ca6b899bd7dd3009fd75ebe0b5a51034"},
+    {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a60c905fd2c15e848ce1fc315fd34d8a9cc72c1dee022a0d8f4c62991131307"},
+    {file = "srsly-2.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:e0b8d5722057000694edf105b8f492e7eb2f3aa6247a5f0c9170d1e0d074151c"},
+    {file = "srsly-2.4.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:196b4261f9d6372d1d3d16d1216b90c7e370b4141471322777b7b3c39afd1210"},
+    {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4750017e6d78590b02b12653e97edd25aefa4734281386cc27501d59b7481e4e"},
+    {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa034cd582ba9e4a120c8f19efa263fcad0f10fc481e73fb8c0d603085f941c4"},
+    {file = "srsly-2.4.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5a78ab9e9d177ee8731e950feb48c57380036d462b49e3fb61a67ce529ff5f60"},
+    {file = "srsly-2.4.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:087e36439af517e259843df93eb34bb9e2d2881c34fa0f541589bcfbc757be97"},
+    {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad141d8a130cb085a0ed3a6638b643e2b591cb98a4591996780597a632acfe20"},
+    {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d05367b2571c0d08d00459636b951e3ca2a1e9216318c157331f09c33489d3"},
+    {file = "srsly-2.4.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3fd661a1c4848deea2849b78f432a70c75d10968e902ca83c07c89c9b7050ab8"},
+    {file = "srsly-2.4.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec37233fe39af97b00bf20dc2ceda04d39b9ea19ce0ee605e16ece9785e11f65"},
+    {file = "srsly-2.4.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2fd4bc081f1d6a6063396b6d97b00d98e86d9d3a3ac2949dba574a84e148080"},
+    {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7347cff1eb4ef3fc335d9d4acc89588051b2df43799e5d944696ef43da79c873"},
+    {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9dc1da5cc94d77056b91ba38365c72ae08556b6345bef06257c7e9eccabafe"},
+    {file = "srsly-2.4.8-cp38-cp38-win_amd64.whl", hash = "sha256:dc0bf7b6f23c9ecb49ec0924dc645620276b41e160e9b283ed44ca004c060d79"},
+    {file = "srsly-2.4.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff8df21d00d73c371bead542cefef365ee87ca3a5660de292444021ff84e3b8c"},
+    {file = "srsly-2.4.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ac3e340e65a9fe265105705586aa56054dc3902789fcb9a8f860a218d6c0a00"},
+    {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d1733f4275eff4448e96521cc7dcd8fdabd68ba9b54ca012dcfa2690db2644"},
+    {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be5b751ad88fdb58fb73871d456248c88204f213aaa3c9aab49b6a1802b3fa8d"},
+    {file = "srsly-2.4.8-cp39-cp39-win_amd64.whl", hash = "sha256:822a38b8cf112348f3accbc73274a94b7bf82515cb14a85ba586d126a5a72851"},
+    {file = "srsly-2.4.8.tar.gz", hash = "sha256:b24d95a65009c2447e0b49cda043ac53fecf4f09e358d87a57446458f91b8a91"},
+]
+
+[package.dependencies]
+catalogue = ">=2.0.3,<2.1.0"
+
 [[package]]
 name = "stack-data"
 version = "0.6.3"
@@ -4051,6 +4571,90 @@ files = [
 [package.dependencies]
 rapidfuzz = ">=3.0.0,<4.0.0"
 
+[[package]]
+name = "thinc"
+version = "8.2.1"
+description = "A refreshing functional take on deep learning, compatible with your favorite libraries"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "thinc-8.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67948bbcf86c3ace8838ca4cdb72977b051d8ee024eeb631d94467be18b15271"},
+    {file = "thinc-8.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e1a558b323f15f60bd79ba3cb95f78945e76748684db00052587270217b96a5"},
+    {file = "thinc-8.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca97679f14f3cd73be76375d6792ac2685c7eca50260cef1810415a2c75ac6c5"},
+    {file = "thinc-8.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:228dabcb8667ff19b2576718e4201b203c3f78dfbed4fa79caab8eef6d5fed48"},
+    {file = "thinc-8.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b02dadc3e41dd5cfd515f0c60aa3e5c472e02c12613a1bb9d837ce5f49cf9d34"},
+    {file = "thinc-8.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0afbcd243d27c076b8c47aded8e5e0aff2ff683af6b95a39839fe3aea862cfd9"},
+    {file = "thinc-8.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4704354879abb052fbd2c658cd6df20d7bba40790ded0e81e994c879849b62f4"},
+    {file = "thinc-8.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6257369950002abe09d64b4f161d10d73af5df3764aea89f70cae018cca14b"},
+    {file = "thinc-8.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2ce2f93a06f8e56796fd2b9d237b6f6ef36ccd9dec66cb38d0092a3947c875"},
+    {file = "thinc-8.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bbefd9939302ebed6d48f57b959be899b23a0c85f1afaf50c82e7b493e5de04"},
+    {file = "thinc-8.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:70fabf9e3d7f4da9804be9d29800dab7506cac12598735edb05ed1cec7b2ee50"},
+    {file = "thinc-8.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0fe6f36faa5a0a69d267d7196d821a9730b3bf1817941db2a83780a199599cd5"},
+    {file = "thinc-8.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a1bc995cace52503c906b87ff0cf428b94435b8b70539c6e6ad29b526925c5"},
+    {file = "thinc-8.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be1f169f01451010822cde5052db3fee25a0793abebe8fbd48d02955a33d0692"},
+    {file = "thinc-8.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9cf766fac7e845e96e509ac9545ea1a60034a069aee3d75068b6e46da084c206"},
+    {file = "thinc-8.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ad99b6d1f7c149137497c6ae9345304fd7465c0c290c00cedd504ff5ae5485d"},
+    {file = "thinc-8.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beda7380017df1fbdf8de1733851464886283786c3c9149e2ac7cef612eff6ed"},
+    {file = "thinc-8.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e6ae6309f110440bcbd6a03b5b4b940d7c607afd2027a6b638336cc42a2171"},
+    {file = "thinc-8.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:aaad5532c3abd2fe69500426a102a3b53725a78eba5ba6867bed9e6b8de0bcba"},
+    {file = "thinc-8.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c32c1e1e60b5e676f1f618915fbb20547b573998693704d0b4987d972e35a62"},
+    {file = "thinc-8.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eae5a3415ff9be0fa21671a58166e82fe6c9ee832252779fd92c31c03692fb7"},
+    {file = "thinc-8.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79e66eed14c2e7b333d69b376f8a091efad366e172b11e39c04814b54969b399"},
+    {file = "thinc-8.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8a1a2ef7061e23507f8172adb7978f7b7bc0bd4ccb266149de7065ee5331e1ea"},
+    {file = "thinc-8.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0216e17be5ddcc1014af55d2e02388698fb64dbc9f32a4782df0a3860615057"},
+    {file = "thinc-8.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:16e7c0988df852cbae40ac03f45e11e3c39300b05dff87267c6fc13108723985"},
+    {file = "thinc-8.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637fafb7d3b51f2aa611371761578fe9999d2675f4fc87eb09e736648d12be30"},
+    {file = "thinc-8.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27bab1026284fba355eda7d83ebc0612ace437fb50ddc9d390e71d732b67e20"},
+    {file = "thinc-8.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:88dab842c68c8e9f0b75a7b4352b53eaa385db2a1de91e276219bfcfda27e47b"},
+    {file = "thinc-8.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5978a97b35a36adb133a83b9fc6cbb9f0c364f8db8525fa0ef5c4fc03f25b889"},
+    {file = "thinc-8.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8181d86b1c8de8dae154ad02399a8d59beb62881c172926594a5f3d7dc0e625"},
+    {file = "thinc-8.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab83ade836933e34a82c61ff9fe0cb3ea9103165935ce9ea12102aff270dad9"},
+    {file = "thinc-8.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19387a23ef2ce2714572040c15f0896b6e0d3751e37ccc1d927c0447f8eac7a1"},
+    {file = "thinc-8.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:229efc84666901730e5575d5ec3c852d02009478411b24c0640f45b42e87a21c"},
+    {file = "thinc-8.2.1.tar.gz", hash = "sha256:cd7fdb3d883a15e6906254e7fb0162f69878e9ccdd1f8519db6ffbfe46bf6f49"},
+]
+
+[package.dependencies]
+blis = ">=0.7.8,<0.8.0"
+catalogue = ">=2.0.4,<2.1.0"
+confection = ">=0.0.1,<1.0.0"
+cymem = ">=2.0.2,<2.1.0"
+murmurhash = ">=1.0.2,<1.1.0"
+numpy = [
+    {version = ">=1.15.0", markers = "python_version < \"3.9\""},
+    {version = ">=1.19.0", markers = "python_version >= \"3.9\""},
+]
+packaging = ">=20.0"
+preshed = ">=3.0.2,<3.1.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0"
+setuptools = "*"
+srsly = ">=2.4.0,<3.0.0"
+wasabi = ">=0.8.1,<1.2.0"
+
+[package.extras]
+cuda = ["cupy (>=5.0.0b4)"]
+cuda-autodetect = ["cupy-wheel (>=11.0.0)"]
+cuda100 = ["cupy-cuda100 (>=5.0.0b4)"]
+cuda101 = ["cupy-cuda101 (>=5.0.0b4)"]
+cuda102 = ["cupy-cuda102 (>=5.0.0b4)"]
+cuda110 = ["cupy-cuda110 (>=5.0.0b4)"]
+cuda111 = ["cupy-cuda111 (>=5.0.0b4)"]
+cuda112 = ["cupy-cuda112 (>=5.0.0b4)"]
+cuda113 = ["cupy-cuda113 (>=5.0.0b4)"]
+cuda114 = ["cupy-cuda114 (>=5.0.0b4)"]
+cuda115 = ["cupy-cuda115 (>=5.0.0b4)"]
+cuda116 = ["cupy-cuda116 (>=5.0.0b4)"]
+cuda117 = ["cupy-cuda117 (>=5.0.0b4)"]
+cuda11x = ["cupy-cuda11x (>=11.0.0)"]
+cuda80 = ["cupy-cuda80 (>=5.0.0b4)"]
+cuda90 = ["cupy-cuda90 (>=5.0.0b4)"]
+cuda91 = ["cupy-cuda91 (>=5.0.0b4)"]
+cuda92 = ["cupy-cuda92 (>=5.0.0b4)"]
+datasets = ["ml-datasets (>=0.2.0,<0.3.0)"]
+mxnet = ["mxnet (>=1.5.1,<1.6.0)"]
+tensorflow = ["tensorflow (>=2.0.0,<2.6.0)"]
+torch = ["torch (>=1.6.0)"]
+
 [[package]]
 name = "threadpoolctl"
 version = "3.2.0"
@@ -4125,6 +4729,26 @@ webencodings = ">=0.4"
 doc = ["sphinx", "sphinx_rtd_theme"]
 test = ["flake8", "isort", "pytest"]
 
+[[package]]
+name = "tldextract"
+version = "5.1.1"
+description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well."
+optional = true
+python-versions = ">=3.8"
+files = [
+    {file = "tldextract-5.1.1-py3-none-any.whl", hash = "sha256:b9c4510a8766d377033b6bace7e9f1f17a891383ced3c5d50c150f181e9e1cc2"},
+    {file = "tldextract-5.1.1.tar.gz", hash = "sha256:9b6dbf803cb5636397f0203d48541c0da8ba53babaf0e8a6feda2d88746813d4"},
+]
+
+[package.dependencies]
+filelock = ">=3.0.8"
+idna = "*"
+requests = ">=2.1.0"
+requests-file = ">=1.4"
+
+[package.extras]
+testing = ["black", "mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ruff", "tox", "types-filelock", "types-requests"]
+
 [[package]]
 name = "toml"
 version = "0.10.2"
@@ -4314,6 +4938,20 @@ platformdirs = ">=3.9.1,<4"
 docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
 test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
 
+[[package]]
+name = "wasabi"
+version = "1.1.2"
+description = "A lightweight console printing and formatting toolkit"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "wasabi-1.1.2-py3-none-any.whl", hash = "sha256:0a3f933c4bf0ed3f93071132c1b87549733256d6c8de6473c5f7ed2e171b5cf9"},
+    {file = "wasabi-1.1.2.tar.gz", hash = "sha256:1aaef3aceaa32edb9c91330d29d3936c0c39fdb965743549c173cb54b16c30b5"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\" and python_version >= \"3.7\""}
+
 [[package]]
 name = "watchdog"
 version = "3.0.0"
@@ -4364,6 +5002,28 @@ files = [
     {file = "wcwidth-0.2.9.tar.gz", hash = "sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"},
 ]
 
+[[package]]
+name = "weasel"
+version = "0.3.4"
+description = "Weasel: A small and easy workflow system"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "weasel-0.3.4-py3-none-any.whl", hash = "sha256:ee48a944f051d007201c2ea1661d0c41035028c5d5a8bcb29a0b10f1100206ae"},
+    {file = "weasel-0.3.4.tar.gz", hash = "sha256:eb16f92dc9f1a3ffa89c165e3a9acd28018ebb656e0da4da02c0d7d8ae3f6178"},
+]
+
+[package.dependencies]
+cloudpathlib = ">=0.7.0,<0.17.0"
+confection = ">=0.0.4,<0.2.0"
+packaging = ">=20.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0"
+requests = ">=2.13.0,<3.0.0"
+smart-open = ">=5.2.1,<7.0.0"
+srsly = ">=2.4.3,<3.0.0"
+typer = ">=0.3.0,<0.10.0"
+wasabi = ">=0.9.1,<1.2.0"
+
 [[package]]
 name = "webencodings"
 version = "0.5.1"
@@ -4661,6 +5321,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
 critique = ["inspiredco"]
 detect-secrets = ["detect-secrets"]
 manifest = ["manifest-ml"]
+pii = ["presidio_analyzer", "presidio_anonymizer"]
 profanity = ["alt-profanity-check"]
 sql = ["sqlalchemy", "sqlglot", "sqlvalidator"]
 summary = ["nltk", "thefuzz"]
@@ -4669,4 +5330,4 @@ vectordb = ["faiss-cpu", "numpy", "tiktoken"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "e66f7e18b083c929dc3b4e5eaf3ea42d0365756218c2417caa3c3517332a727a"
+content-hash = "2e17825249a926c61383f69b7d14fd0819471e475035e10d0ad3264f72273b2f"
diff --git a/pyproject.toml b/pyproject.toml
index 57212929f..269742a84 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,8 @@ alt-profanity-check = {version = "^1.3.1", optional = true}
 detect-secrets = {version = "^1.4.0", optional = true}
 manifest-ml = {version = "^0.1.8", optional = true}
 inspiredco = {version = "^0.0.2", optional = true}
+presidio_analyzer = {version = "^2.2.33", optional = true}
+presidio_anonymizer = {version = "^2.2.33", optional = true}
 
 [tool.poetry.extras]
 sql = ["sqlvalidator", "sqlalchemy", "sqlglot"]
@@ -49,6 +51,7 @@ profanity = ["alt-profanity-check"]
 detect-secrets = ["detect-secrets"]
 manifest = ["manifest-ml"]
 critique = ["inspiredco"]
+pii = ["presidio_analyzer", "presidio_anonymizer"]
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.4.3"


Test patch:
diff --git a/tests/integration_tests/mock_llm_outputs.py b/tests/integration_tests/mock_llm_outputs.py
index 11e3d2bbf..33e980f3e 100644
--- a/tests/integration_tests/mock_llm_outputs.py
+++ b/tests/integration_tests/mock_llm_outputs.py
@@ -78,6 +78,10 @@ def _invoke_llm(
                 python_rail.COMPILED_PROMPT_1_WITHOUT_INSTRUCTIONS,
                 python_rail.COMPILED_INSTRUCTIONS,
             ): python_rail.LLM_OUTPUT_1_FAIL_GUARDRAILS_VALIDATION,
+            (
+                python_rail.COMPILED_PROMPT_1_PYDANTIC_2_WITHOUT_INSTRUCTIONS,
+                python_rail.COMPILED_INSTRUCTIONS,
+            ): python_rail.LLM_OUTPUT_1_FAIL_GUARDRAILS_VALIDATION,
             (
                 python_rail.COMPILED_PROMPT_2_WITHOUT_INSTRUCTIONS,
                 python_rail.COMPILED_INSTRUCTIONS,
diff --git a/tests/integration_tests/mock_presidio.py b/tests/integration_tests/mock_presidio.py
new file mode 100644
index 000000000..e18ebc884
--- /dev/null
+++ b/tests/integration_tests/mock_presidio.py
@@ -0,0 +1,57 @@
+from typing import List
+
+PII_ENTITIES_MAP = {
+    "pii": [
+        "EMAIL_ADDRESS",
+        "PHONE_NUMBER",
+        "DOMAIN_NAME",
+        "IP_ADDRESS",
+        "DATE_TIME",
+        "LOCATION",
+        "PERSON",
+        "URL",
+    ],
+    "spi": [
+        "CREDIT_CARD",
+        "CRYPTO",
+        "IBAN_CODE",
+        "NRP",
+        "MEDICAL_LICENSE",
+        "US_BANK_NUMBER",
+        "US_DRIVER_LICENSE",
+        "US_ITIN",
+        "US_PASSPORT",
+        "US_SSN",
+    ],
+}
+
+
+class MockAnalyzerEngine:
+    """Mocks the AnalyzerEngine class from presidio-analyzer."""
+
+    def __init__(self) -> None:
+        pass
+
+
+class MockAnonymizerEngine:
+    """Mocks the AnonymizerEngine class from presidio-anonymizer."""
+
+    def __init__(self) -> None:
+        pass
+
+
+def mock_anonymize(self, text: str, entities: List[str]) -> str:
+    output = None
+    if text == "My email address is demo@lol.com, and my phone number is 1234567890":
+        if entities == [
+            "EMAIL_ADDRESS",
+            "PHONE_NUMBER",
+        ] or entities == PII_ENTITIES_MAP.get("pii"):
+            output = "My email address is <EMAIL_ADDRESS>, and my phone number is <PHONE_NUMBER>"  # noqa
+        elif entities == ["EMAIL_ADDRESS"]:
+            output = (
+                "My email address is <EMAIL_ADDRESS>, and my phone number is 1234567890"
+            )
+    elif text == "My email address is xyz and my phone number is unavailable.":
+        output = text
+    return output
diff --git a/tests/integration_tests/test_assets/pydantic/validated_response_reask.py b/tests/integration_tests/test_assets/pydantic/validated_response_reask.py
index 83e25307c..f4a6125da 100644
--- a/tests/integration_tests/test_assets/pydantic/validated_response_reask.py
+++ b/tests/integration_tests/test_assets/pydantic/validated_response_reask.py
@@ -3,6 +3,7 @@
 
 from pydantic import BaseModel, Field
 
+from guardrails.utils.pydantic_utils import PYDANTIC_VERSION
 from guardrails.utils.reask_utils import FieldReAsk
 from guardrails.validators import (
     FailResult,
@@ -59,14 +60,31 @@ class Person(BaseModel):
     """
 
     name: str
-    age: int = Field(..., validators=[AgeMustBeBetween0And150(on_fail="reask")])
-    zip_code: str = Field(
-        ...,
-        validators=[
-            ZipCodeMustBeNumeric(on_fail="reask"),
-            ZipCodeInCalifornia(on_fail="reask"),
-        ],
-    )
+    if PYDANTIC_VERSION.startswith("1"):
+        age: int = Field(..., validators=[AgeMustBeBetween0And150(on_fail="reask")])
+        zip_code: str = Field(
+            ...,
+            validators=[
+                ZipCodeMustBeNumeric(on_fail="reask"),
+                ZipCodeInCalifornia(on_fail="reask"),
+            ],
+        )
+    else:
+        age: int = Field(
+            ...,
+            json_schema_extra={
+                "validators": [AgeMustBeBetween0And150(on_fail="reask")]
+            },
+        )
+        zip_code: str = Field(
+            ...,
+            json_schema_extra={
+                "validators": [
+                    ZipCodeMustBeNumeric(on_fail="reask"),
+                    ZipCodeInCalifornia(on_fail="reask"),
+                ],
+            },
+        )
 
 
 class ListOfPeople(BaseModel):
diff --git a/tests/integration_tests/test_assets/python_rail/__init__.py b/tests/integration_tests/test_assets/python_rail/__init__.py
index f845e1895..2fed06291 100644
--- a/tests/integration_tests/test_assets/python_rail/__init__.py
+++ b/tests/integration_tests/test_assets/python_rail/__init__.py
@@ -7,6 +7,9 @@
 )
 
 COMPILED_PROMPT_1_WITHOUT_INSTRUCTIONS = reader("compiled_prompt_1.txt")
+COMPILED_PROMPT_1_PYDANTIC_2_WITHOUT_INSTRUCTIONS = reader(
+    "compiled_prompt_1_pydantic_2.txt"
+)
 COMPILED_PROMPT_2_WITHOUT_INSTRUCTIONS = reader("compiled_prompt_2.txt")
 COMPILED_INSTRUCTIONS = reader("compiled_instructions.txt")
 
diff --git a/tests/integration_tests/test_assets/python_rail/compiled_prompt_1_pydantic_2.txt b/tests/integration_tests/test_assets/python_rail/compiled_prompt_1_pydantic_2.txt
new file mode 100644
index 000000000..2120ed2ce
--- /dev/null
+++ b/tests/integration_tests/test_assets/python_rail/compiled_prompt_1_pydantic_2.txt
@@ -0,0 +1,34 @@
+Provide detailed information about the top 5 grossing movies from Christopher Nolan including release date, duration, budget, whether it's a sequel, website, and contact email.
+
+Given below is XML that describes the information to extract from this document and the tags to extract it into.
+
+<output>
+    <string name="name" format="is-valid-director"/>
+    <list name="movies">
+        <object>
+            <integer name="rank"/>
+            <string name="title"/>
+            <object name="details">
+                <date name="release_date"/>
+                <time name="duration"/>
+                <float name="budget"/>
+                <bool name="is_sequel" required="false"/>
+                <string name="website" format="length: min=9 max=100"/>
+                <string name="contact_email"/>
+                <choice name="revenue" discriminator="revenue_type">
+                    <case name="box_office">
+                        <float name="gross"/>
+                        <float name="opening_weekend"/>
+                    </case>
+                    <case name="streaming">
+                        <integer name="subscriptions"/>
+                        <float name="subscription_fee"/>
+                    </case>
+                </choice>
+            </object>
+        </object>
+    </list>
+</output>
+
+
+ONLY return a valid JSON object (no other text is necessary), where the key of the field in JSON is the `name` attribute of the corresponding XML, and the value is of the type specified by the corresponding XML's tag. The JSON MUST conform to the XML format, including any types and format requests e.g. requests for lists, objects and specific types. Be correct and concise. If you are unsure anywhere, enter `null`.
diff --git a/tests/integration_tests/test_pydantic.py b/tests/integration_tests/test_pydantic.py
index 2109a4f51..13c9dba82 100644
--- a/tests/integration_tests/test_pydantic.py
+++ b/tests/integration_tests/test_pydantic.py
@@ -1,4 +1,9 @@
+import json
+from typing import Dict, List
+
 import openai
+import pytest
+from pydantic import BaseModel
 
 import guardrails as gd
 
@@ -91,3 +96,48 @@ def test_pydantic_with_full_schema_reask(mocker):
     )
     assert guard_history[2].output == pydantic.LLM_OUTPUT_FULL_REASK_2
     assert guard_history[2].validated_output == pydantic.VALIDATED_OUTPUT_REASK_3
+
+
+class ContainerModel(BaseModel):
+    annotated_dict: Dict[str, str] = {}
+    annotated_dict_in_list: List[Dict[str, str]] = []
+    annotated_list: List[str] = []
+    annotated_list_in_dict: Dict[str, List[str]] = {}
+
+
+class ContainerModel2(BaseModel):
+    dict_: Dict = {}
+    dict_in_list: List[Dict] = []
+    list_: List = []
+    list_in_dict: Dict[str, List] = {}
+
+
+@pytest.mark.parametrize(
+    "model, output",
+    [
+        (
+            ContainerModel,
+            {
+                "annotated_dict": {"a": "b"},
+                "annotated_dict_in_list": [{"a": "b"}],
+                "annotated_list": ["a"],
+                "annotated_list_in_dict": {"a": ["b"]},
+            },
+        ),
+        (
+            ContainerModel2,
+            {
+                "dict_": {"a": "b"},
+                "dict_in_list": [{"a": "b"}],
+                "list_": ["a"],
+                "list_in_dict": {"a": ["b"]},
+            },
+        ),
+    ],
+)
+def test_container_types(model, output):
+    output_str = json.dumps(output)
+
+    guard = gd.Guard.from_pydantic(model)
+    out = guard.parse(output_str)
+    assert out == output
diff --git a/tests/integration_tests/test_python_rail.py b/tests/integration_tests/test_python_rail.py
index 307478f6a..cb6bc732e 100644
--- a/tests/integration_tests/test_python_rail.py
+++ b/tests/integration_tests/test_python_rail.py
@@ -4,10 +4,10 @@
 
 import openai
 import pytest
-from pydantic import BaseModel, Field, root_validator, validator
+from pydantic import BaseModel, Field
 
 import guardrails as gd
-from guardrails.utils.pydantic_utils import add_validator
+from guardrails.utils.pydantic_utils import PYDANTIC_VERSION, add_validator
 from guardrails.validators import (
     FailResult,
     PassResult,
@@ -52,7 +52,16 @@ class BoxOfficeRevenue(BaseModel):
         opening_weekend: float
 
         # Field-level validation using Pydantic (not Guardrails)
-        @validator("gross")
+        if PYDANTIC_VERSION.startswith("1"):
+            from pydantic import validator
+
+            decorator = validator("gross")
+        else:
+            from pydantic import field_validator
+
+            decorator = field_validator("gross")
+
+        @decorator
         def validate_gross(cls, gross):
             if gross <= 0:
                 raise ValueError("Gross revenue must be a positive value")
@@ -68,23 +77,47 @@ class Details(BaseModel):
         duration: time
         budget: float
         is_sequel: bool = Field(default=False)
-        website: str = Field(validators=[ValidLength(min=9, max=100, on_fail="reask")])
+
+        # Root-level validation using Pydantic (Not in Guardrails)
+        if PYDANTIC_VERSION.startswith("1"):
+            website: str = Field(
+                validators=[ValidLength(min=9, max=100, on_fail="reask")]
+            )
+            from pydantic import root_validator
+
+            @root_validator
+            def validate_budget_and_gross(cls, values):
+                budget = values.get("budget")
+                revenue = values.get("revenue")
+                if isinstance(revenue, BoxOfficeRevenue):
+                    gross = revenue.gross
+                    if budget >= gross:
+                        raise ValueError("Budget must be less than gross revenue")
+                return values
+
+        else:
+            website: str = Field(
+                json_schema_extra={
+                    "validators": [ValidLength(min=9, max=100, on_fail="reask")]
+                }
+            )
+            from pydantic import model_validator
+
+            @model_validator(mode="before")
+            def validate_budget_and_gross(cls, values):
+                budget = values.get("budget")
+                revenue = values.get("revenue")
+                if revenue["revenue_type"] == "box_office":
+                    gross = revenue["gross"]
+                    if budget >= gross:
+                        raise ValueError("Budget must be less than gross revenue")
+                return values
+
         contact_email: str
         revenue: Union[BoxOfficeRevenue, StreamingRevenue] = Field(
             ..., discriminator="revenue_type"
         )
 
-        # Root-level validation using Pydantic (Not in Guardrails)
-        @root_validator
-        def validate_budget_and_gross(cls, values):
-            budget = values.get("budget")
-            revenue = values.get("revenue")
-            if isinstance(revenue, BoxOfficeRevenue):
-                gross = revenue.gross
-                if budget >= gross:
-                    raise ValueError("Budget must be less than gross revenue")
-            return values
-
     class Movie(BaseModel):
         rank: int
         title: str
@@ -125,9 +158,15 @@ class Director(BaseModel):
     # Check that the guard state object has the correct number of re-asks.
     assert len(guard_history) == 2
 
-    assert guard_history[0].prompt == gd.Prompt(
-        python_rail.COMPILED_PROMPT_1_WITHOUT_INSTRUCTIONS
-    )
+    if PYDANTIC_VERSION.startswith("1"):
+        assert guard_history[0].prompt == gd.Prompt(
+            python_rail.COMPILED_PROMPT_1_WITHOUT_INSTRUCTIONS
+        )
+    else:
+        assert guard_history[0].prompt == gd.Prompt(
+            python_rail.COMPILED_PROMPT_1_PYDANTIC_2_WITHOUT_INSTRUCTIONS
+        )
+
     assert (
         guard_history[0].output == python_rail.LLM_OUTPUT_1_FAIL_GUARDRAILS_VALIDATION
     )
@@ -140,20 +179,32 @@ class Director(BaseModel):
         == python_rail.LLM_OUTPUT_2_SUCCEED_GUARDRAILS_BUT_FAIL_PYDANTIC_VALIDATION
     )
 
-    with pytest.raises(ValueError):
-        Director.parse_raw(
-            python_rail.LLM_OUTPUT_2_SUCCEED_GUARDRAILS_BUT_FAIL_PYDANTIC_VALIDATION
-        )
+    if PYDANTIC_VERSION.startswith("1"):
+        with pytest.raises(ValueError):
+            Director.parse_raw(
+                python_rail.LLM_OUTPUT_2_SUCCEED_GUARDRAILS_BUT_FAIL_PYDANTIC_VALIDATION
+            )
 
-    # The user can take corrective action based on the failed validation.
-    # Either manipulating the output themselves, taking corrective action
-    # in their application, or upstreaming their validations into Guardrails.
+        # The user can take corrective action based on the failed validation.
+        # Either manipulating the output themselves, taking corrective action
+        # in their application, or upstreaming their validations into Guardrails.
 
-    # The fixed output should pass validation using Pydantic
-    Director.parse_raw(python_rail.LLM_OUTPUT_3_SUCCEED_GUARDRAILS_AND_PYDANTIC)
+        # The fixed output should pass validation using Pydantic
+        Director.parse_raw(python_rail.LLM_OUTPUT_3_SUCCEED_GUARDRAILS_AND_PYDANTIC)
+    else:
+        with pytest.raises(ValueError):
+            Director.model_validate_json(
+                python_rail.LLM_OUTPUT_2_SUCCEED_GUARDRAILS_BUT_FAIL_PYDANTIC_VALIDATION
+            )
+        Director.model_validate_json(
+            python_rail.LLM_OUTPUT_3_SUCCEED_GUARDRAILS_AND_PYDANTIC
+        )
 
 
+@pytest.mark.skipif(not PYDANTIC_VERSION.startswith("1"), reason="Pydantic 1.x only")
 def test_python_rail_add_validator(mocker):
+    from pydantic import root_validator, validator
+
     mocker.patch(
         "guardrails.llm_providers.OpenAIChatCallable",
         new=MockOpenAIChatCallable,
diff --git a/tests/integration_tests/test_validators.py b/tests/integration_tests/test_validators.py
index 3ff938b69..13becbd8b 100644
--- a/tests/integration_tests/test_validators.py
+++ b/tests/integration_tests/test_validators.py
@@ -8,10 +8,11 @@
 from guardrails.datatypes import DataType
 from guardrails.schema import StringSchema
 from guardrails.validator_base import PassResult, ValidationResult
-from guardrails.validators import DetectSecrets, SimilarToList
+from guardrails.validators import DetectSecrets, PIIFilter, SimilarToList
 
 from .mock_embeddings import MOCK_EMBEDDINGS
 from .mock_llm_outputs import MockOpenAICallable
+from .mock_presidio import MockAnalyzerEngine, MockAnonymizerEngine, mock_anonymize
 from .mock_secrets import (
     EXPECTED_SECRETS_CODE_SNIPPET,
     NO_SECRETS_CODE_SNIPPET,
@@ -53,17 +54,17 @@ def embed_function(text: str):
 
     # 1. Test for integer values
     # 1.1 Test for values within the standard deviation
-    val = 3
+    val = "3"
     output = guard.parse(
         llm_output=val,
         metadata={"prev_values": int_prev_values},
     )
-    assert int(output) == val
+    assert output == val
 
     # 1.2 Test not passing prev_values
     # Should raise ValueError
     with pytest.raises(ValueError):
-        val = 3
+        val = "3"
         output = guard.parse(
             llm_output=val,
         )
@@ -71,14 +72,14 @@ def embed_function(text: str):
     # 1.3 Test passing str prev values for int val
     # Should raise ValueError
     with pytest.raises(ValueError):
-        val = 3
+        val = "3"
         output = guard.parse(
             llm_output=val,
             metadata={"prev_values": [str(i) for i in int_prev_values]},
         )
 
     # 1.4 Test for values outside the standard deviation
-    val = 300
+    val = "300"
     output = guard.parse(
         llm_output=val,
         metadata={"prev_values": int_prev_values},
@@ -186,6 +187,153 @@ def test_detect_secrets(mocker):
     assert not os.path.exists("temp.txt")
 
 
+def test_pii_filter(mocker):
+    """Integration test for PIIFilter."""
+
+    # Mock the the intialisations of AnalyzerEngine and AnonymizerEngine
+    mocker.patch("guardrails.validators.AnalyzerEngine", new=MockAnalyzerEngine)
+    mocker.patch("guardrails.validators.AnonymizerEngine", new=MockAnonymizerEngine)
+
+    # Mock the analyze and anomymize functions
+    mocker.patch(
+        "guardrails.validators.PIIFilter.get_anonymized_text", new=mock_anonymize
+    )
+
+    # ------------------
+    # 1. Initialise Guard from string with setting pii_entities as a string
+    # Also check whether all parameters are correctly initialised
+    guard = Guard.from_string(
+        validators=[PIIFilter(pii_entities="pii", on_fail="fix")],
+        description="testmeout",
+    )
+
+    # Do parse call
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+    output = guard.parse(
+        llm_output=text,
+    )
+    # Validated output should be different from input
+    assert output != text
+
+    # Validated output should contain masked pii entities
+    assert all(entity in output for entity in ["<EMAIL_ADDRESS>", "<PHONE_NUMBER>"])
+
+    # ------------------
+    # 2. Initialise Guard from string with setting pii_entities as a list
+    # Also check whether all parameters are correctly initialised
+    guard = Guard.from_string(
+        validators=[
+            PIIFilter(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="fix")
+        ],
+        description="testmeout",
+    )
+
+    # Do parse call
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+    output = guard.parse(
+        llm_output=text,
+    )
+    # Validated output should be different from input
+    assert output != text
+
+    # Validated output should contain masked pii entities
+    assert all(entity in output for entity in ["<EMAIL_ADDRESS>", "<PHONE_NUMBER>"])
+
+    # Check with text without any pii entities
+    text = "My email address is xyz and my phone number is unavailable."
+    output = guard.parse(
+        llm_output=text,
+    )
+    # Validated output should be same as input
+    assert output == text
+
+    # ------------------
+    # 3. Initialise Guard from string without setting pii_entities
+    # Also don't pass through metadata
+    # Should raise ValueError
+    guard = Guard.from_string(
+        validators=[PIIFilter(on_fail="fix")],
+        description="testmeout",
+    )
+
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+    with pytest.raises(ValueError):
+        output = guard.parse(
+            llm_output=text,
+        )
+
+    # ------------------
+    # 4. Initialise Guard from string without setting pii_entities
+    guard = Guard.from_string(
+        validators=[PIIFilter(on_fail="fix")],
+        description="testmeout",
+    )
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+
+    # Now try with string of pii entities passed through metadata
+    output = guard.parse(
+        llm_output=text,
+        metadata={"pii_entities": "pii"},
+    )
+    # Validated output should be different from input
+    assert output != text
+
+    # Validated output should contain masked pii entities
+    assert all(entity in output for entity in ["<EMAIL_ADDRESS>", "<PHONE_NUMBER>"])
+
+    # Now try with list of pii entities passed through metadata
+    output = guard.parse(
+        llm_output=text,
+        metadata={"pii_entities": ["EMAIL_ADDRESS", "PHONE_NUMBER"]},
+    )
+    # Validated output should be different from input
+    assert output != text
+
+    # Validated output should contain masked pii entities
+    assert all(entity in output for entity in ["<EMAIL_ADDRESS>", "<PHONE_NUMBER>"])
+
+    # ------------------
+    # 5. Initialise Guard from string setting
+    # pii_entities as a string "pii" -> all entities
+    # But also pass in metadata with all pii_entities as a list
+    # only containing EMAIL_ADDRESS
+    # metadata should override the pii_entities passed in the constructor,
+    # and only mask in EMAIL_ADDRESS
+
+    guard = Guard.from_string(
+        validators=[PIIFilter(pii_entities="pii", on_fail="fix")],
+        description="testmeout",
+    )
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+
+    output = guard.parse(
+        llm_output=text,
+        metadata={"pii_entities": ["EMAIL_ADDRESS"]},
+    )
+    # Validated output should be different from input
+    assert output != text
+
+    # Validated output should contain masked EMAIL_ADDRESS
+    # and not PHONE_NUMBER
+    assert "<EMAIL_ADDRESS>" in output
+    assert "<PHONE_NUMBER>" not in output
+
+    # ------------------
+    # 6. Initialise Guard from string setting an incorrect string of pii_entities
+    # Should raise ValueError during validate
+
+    guard = Guard.from_string(
+        validators=[PIIFilter(pii_entities="piii", on_fail="fix")],
+        description="testmeout",
+    )
+    text = "My email address is demo@lol.com, and my phone number is 1234567890"
+
+    with pytest.raises(ValueError):
+        output = guard.parse(
+            llm_output=text,
+        )
+
+
 @register_validator("mycustominstancecheckvalidator", data_type="string")
 class MyValidator(Validator):
     def __init__(
diff --git a/tests/unit_tests/test_llm_providers.py b/tests/unit_tests/test_llm_providers.py
index 5440b9bd2..76951df0b 100644
--- a/tests/unit_tests/test_llm_providers.py
+++ b/tests/unit_tests/test_llm_providers.py
@@ -283,7 +283,7 @@ async def return_async():
 
 class ReturnTempCallable(Callable):
     def __call__(*args, **kwargs) -> Any:
-        return kwargs.get("temperature")
+        return ""
 
 
 @pytest.mark.parametrize(
@@ -295,7 +295,8 @@ def __call__(*args, **kwargs) -> Any:
 )
 def test_get_llm_ask_temperature(llm_api, args, kwargs, expected_temperature):
     result = get_llm_ask(llm_api, *args, **kwargs)
-    assert result().output == str(expected_temperature)
+    assert "temperature" in result.init_kwargs
+    assert result.init_kwargs["temperature"] == expected_temperature
 
 
 def test_chat_prompt():
diff --git a/tests/unit_tests/utils/test_pydantic_utils.py b/tests/unit_tests/utils/test_pydantic_utils.py
index 5af23490e..7c8f1f503 100644
--- a/tests/unit_tests/utils/test_pydantic_utils.py
+++ b/tests/unit_tests/utils/test_pydantic_utils.py
@@ -1,12 +1,18 @@
+import pytest
 from pydantic import BaseModel, Field
 
 from guardrails.utils.pydantic_utils import (
+    PYDANTIC_VERSION,
     add_pydantic_validators_as_guardrails_validators,
     add_validator,
 )
 from guardrails.validators import FailResult, PassResult, ValidChoices, ValidLength
 
 
+@pytest.mark.skipif(
+    not PYDANTIC_VERSION.startswith("1"),
+    reason="Tests validators syntax for Pydantic v1",
+)
 def test_add_pydantic_validators_as_guardrails_validators():
     # TODO(shreya): Uncomment when custom validators are supported
     # def dummy_validator(name: str):
@@ -75,3 +81,33 @@ class DummyModel(BaseModel):
     # validators[3].validate(None, "Bob", None)
     # with pytest.raises(EventDetail):
     #     validators[3].validate(None, "Alex", None)
+
+
+@pytest.mark.skipif(
+    not PYDANTIC_VERSION.startswith("2"),
+    reason="Tests validators syntax for Pydantic v2",
+)
+def test_add_pydantic_validators_as_guardrails_validators_v2():
+    class DummyModel(BaseModel):
+        name: str = Field(..., validators=[ValidLength(min=1, max=10)])
+
+    model_fields = add_pydantic_validators_as_guardrails_validators(DummyModel)
+    name_field = model_fields["name"]
+
+    # Should have 1 field
+    assert len(model_fields) == 1, "Should only have one field"
+
+    # Should have 4 validators: 1 from the field, 2 from the add_validator method,
+    # and 1 from the validator decorator.
+    validators = name_field.json_schema_extra["validators"]
+    assert len(validators) == 1, "Should have 1 validator"
+
+    # The BaseModel field should not be modified
+    assert len(DummyModel.model_fields["name"].json_schema_extra["validators"]) == 1
+
+    # The first validator should be the ValidLength validator
+    assert isinstance(
+        validators[0], ValidLength
+    ), "First validator should be ValidLength"
+    assert isinstance(validators[0].validate("Beatrice", {}), PassResult)
+    assert isinstance(validators[0].validate("MrAlexander", {}), FailResult)