Enhancing Robustness: Leveraging Pydantic Validation Decorator with ActionWeaver

The Pydantic [@validate_call decorator](https://docs.pydantic.dev/latest/concepts/validation_decorator/) enables the validation of function arguments based on the function’s annotations before the function is executed.

ActionWeaver, as a framework centered on function calls, oversees the function calling process for users. Specifically, it includes an ExceptionHandler, empowering users to specify behaviors to exceptions. Together, these components form a powerful combination for developing robust LLM applications.

[1]:
%load_ext autoreload
%autoreload 2
[2]:
import os

from typing import List
from uuid import UUID, uuid4

from actionweaver import action
from actionweaver.utils.tokens import TokenUsageTracker
from actionweaver.llms import wrap, ExceptionHandler, ExceptionAction, ChatLoopInfo, Continue, Return
from actionweaver.actions.factories.pydantic_model_to_action import  action_from_model

from pydantic import BaseModel, Field, PrivateAttr, validate_call, field_validator

from openai import OpenAI

from datetime import datetime

from openai import AzureOpenAI, OpenAI

Let’s instantiate an OpenAI client and then encapsulate it within an ActionWeaver wrapper. ActionWeaver will handle the function calling loop.

[3]:
# Azure OpenAI
# MODEL="gpt-4-32k"
# llm = AzureOpenAI(
#     azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT"),
#     api_key=os.getenv("AZURE_OPENAI_KEY"),
#     api_version="2023-10-01-preview"
# )

# OpenAI
MODEL = "gpt-4"
llm = wrap(OpenAI())

Here, we’ll ask LLM to use the ingest_user_info function, which accepts Pydantic model UserModel as arguments. We aim for the LLM to extract user information from natural language and trigger ingest_user_info, which will then store the validated user information into user_db.

Within the Pydantic model UserModel, we’ve specified certain field_validators to ensure that both the name and phone_number adhere to specific formats.

[4]:
import re
from pydantic import BaseModel, ValidationError, validator

class UserModel(BaseModel):
    _uid: UUID = PrivateAttr(default_factory=uuid4)
    _created_at: datetime = PrivateAttr(default_factory=datetime.now)
    name: str
    phone_number: str

    @field_validator('name')
    @classmethod
    def validate_name(cls, v: str) -> str:
        # Split the input string into first and last names
        names = v.split()

        # Check if both first and last names are present
        if len(names) != 2:
            raise ValueError('Name must contain a first name and a last name separated by a space')

        # Check if the formatted name is not in uppercase
        if v != v.upper():
            raise ValueError('Name must be in uppercase')

        return v

    @field_validator('phone_number')
    @classmethod
    def validate_phone_number(cls, v: str) -> str:
        # Define a regular expression pattern for a phone number with country code
        pattern = r'^\+\d{1,3}\s*\(\d{3}\)\s*\d{3}-\d{4}$'  # Example: +1 (XXX) XXX-XXXX

        # Check if the phone number matches the pattern
        if not re.match(pattern, v):
            raise ValueError('phone number must be in the format +1 (XXX) XXX-XXXX')
        return v

user_db = []

@action(name="SaveUserInfo", stop=True)
@validate_call
def ingest_user_info(users: List[UserModel]):
    """Save user info to database"""
    user_db.append(users)
    return "success"

Now, let’s attempt to prompt the LLM to call ingest_user_info. We use the following syntax to force the LLM to call function:

ingest_user_info.invoke(
    llm,
    messages=messages,
    model=MODEL,
    stream=False,
    temperature=1,
    exception_handler = ExceptionRetryHandler(3)
)

For further information about this syntax, refer to the documentation at: https://github.com/TengHu/ActionWeaver?tab=readme-ov-file#force-execution-of-an-action


As you’ll notice, the input text has a different format and may not pass the field validation.

To enable the LLM to handle this, we’ll define an ExceptionRetryHandler and pass it as argument. This handler will take the exception message as input to the LLM and allow for a maximum number of retries. It empowers the LLM to “self-heal” using the error message and return the desired result.

[5]:
class ExceptionRetryHandler(ExceptionHandler):
    def __init__(self, max_retry=2):
        self.max_retry = max_retry

    def handle_exception(self, e: Exception, info: ChatLoopInfo) -> ExceptionAction:
        if self.max_retry:
            self.max_retry -= 1

            print(f"\nRetrying. Retries left: {self.max_retry}")
            print(f"Exception raised: {type(e).__name__}: {str(e)}")

            response = info.context['response']
            messages = info.context['messages']
            messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": response.choices[0].message.tool_calls[0].id,
                        "name": response.choices[0].message.tool_calls[0].function.name,
                        "content": f"Exceptions raised: \n{e}",
                    }
                )

            return Continue(functions=info.context['tools'])
        raise e

input = """                Name       Phone Number
0  Dr. Danielle King      (844)055-3780
1        John Miller  +1-268-920-5475x5
2    Michael Johnson  +1-758-232-6153x8
"""

messages = [
    {"role": "user", "content": input}
]

response = ingest_user_info.invoke(
    llm,
    messages=messages,
    model=MODEL,
    stream=False,
    temperature=1,
    exception_handler = ExceptionRetryHandler(3)
)

Retrying. Retries left: 2
Exception raised: ValidationError: 6 validation errors for ingest_user_info
users.0.name
  Value error, Name must contain a first name and a last name separated by a space [type=value_error, input_value='Dr. Danielle King', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
users.0.phone_number
  Value error, phone number must be in the format +1 (XXX) XXX-XXXX [type=value_error, input_value='(844)055-3780', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
users.1.name
  Value error, Name must be in uppercase [type=value_error, input_value='John Miller', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
users.1.phone_number
  Value error, phone number must be in the format +1 (XXX) XXX-XXXX [type=value_error, input_value='+1-268-920-5475x5', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
users.2.name
  Value error, Name must be in uppercase [type=value_error, input_value='Michael Johnson', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
users.2.phone_number
  Value error, phone number must be in the format +1 (XXX) XXX-XXXX [type=value_error, input_value='+1-758-232-6153x8', input_type=str]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error
[6]:
user_db
[6]:
[[UserModel(name='DANIELLE KING', phone_number='+1 (844) 055-3780'),
  UserModel(name='JOHN MILLER', phone_number='+1 (268) 920-5475'),
  UserModel(name='MICHAEL JOHNSON', phone_number='+1 (758) 232-6153')]]