diff --git a/docs/maybe.md b/docs/maybe.md new file mode 100644 index 000000000..d0213931b --- /dev/null +++ b/docs/maybe.md @@ -0,0 +1,32 @@ +# Handling Errors Within Function Calls + +You can create a wrapper class to hold either the result of an operation or an error message. This allows you to remain within a function call even if an error occurs, facilitating better error handling without breaking the code flow. + +```python +class UserDetail(BaseModel): + age: int + name: str + role: Optional[str] = Field(default=None) + +class MaybeUser(BaseModel): + result: Optional[UserDetail] = Field(default=None) + error: bool = Field(default=False) + message: Optional[str] + + def __bool__(self): + return self.result is not None +``` + +With the `MaybeUser` class, you can either receive a `UserDetail` object in result or get an error message in message. + +## Simplification with the Maybe Pattern + +You can further simplify this using instructor to create the `Maybe` pattern. + +```python +import instructor + +MaybeUser = instructor.Maybe(UserDetail) +``` + +This allows you to quickly create a Maybe type for any class, streamlining the process. \ No newline at end of file diff --git a/docs/tips/index.md b/docs/tips/index.md index e5b6c1346..04904db65 100644 --- a/docs/tips/index.md +++ b/docs/tips/index.md @@ -42,6 +42,39 @@ class UserDetail(BaseModel): ``` +## Handling Errors Within Function Calls + +You can create a wrapper class to hold either the result of an operation or an error message. This allows you to remain within a function call even if an error occurs, facilitating better error handling without breaking the code flow. + +```python +class UserDetail(BaseModel): + age: int + name: str + role: Optional[str] = Field(default=None) + +class MaybeUser(BaseModel): + result: Optional[UserDetail] = Field(default=None) + error: bool = Field(default=False) + message: Optional[str] + + def __bool__(self): + return self.result is not None +``` + +With the `MaybeUser` class, you can either receive a `UserDetail` object in result or get an error message in message. + +### Simplification with the Maybe Pattern + +You can further simplify this using instructor to create the `Maybe` pattern dynamically from any `BaseModel`. + +```python +import instructor + +MaybeUser = instructor.Maybe(UserDetail) +``` + +This allows you to quickly create a Maybe type for any class, streamlining the process. + ## Tips for Enumerations To prevent data misalignment, use Enums for standardized fields. Always include an "Other" option as a fallback so the model can signal uncertainty. @@ -181,4 +214,5 @@ class TimeRange(BaseModel): chain_of_thought: str = Field(..., description="Step by step reasoning to get the correct time range") start_time: int = Field(..., description="The start time in hours.") end_time: int = Field(..., description="The end time in hours.") -``` \ No newline at end of file +``` + diff --git a/examples/simple-extraction/maybe_user.py b/examples/simple-extraction/maybe_user.py new file mode 100644 index 000000000..5465bcc42 --- /dev/null +++ b/examples/simple-extraction/maybe_user.py @@ -0,0 +1,77 @@ +import instructor +import openai +from pydantic import BaseModel, Field +from typing import Optional, Type + +instructor.patch() + + +class UserDetail(BaseModel): + age: int + name: str + role: Optional[str] = Field(default=None) + + +MaybeUser = instructor.Maybe(UserDetail) + + +def get_user_detail(string) -> MaybeUser: # type: ignore + return openai.ChatCompletion.create( + model="gpt-3.5-turbo-0613", + response_model=MaybeUser, + messages=[ + { + "role": "user", + "content": f"Get user details for {string}", + }, + ], + ) # type: ignore + + +user = get_user_detail("Jason is 25 years old") +print(user.model_dump_json(indent=2)) +""" +{ + "user": { + "age": 25, + "name": "Jason", + "role": null + }, + "error": false, + "message": null +} +""" + +user = get_user_detail("Jason is a 25 years old scientist") +print(user.model_dump_json(indent=2)) +""" +{ + "user": { + "age": 25, + "name": "Jason", + "role": "scientist" + }, + "error": false, + "message": null +} +""" + +# ! notice that the string should not contain anything +# ! but a user and age was still extracted ?! +user = get_user_detail("User not found") +print(user.model_dump_json(indent=2)) +""" +{ + "user": null, + "error": true, + "message": "User not found" +} +""" + +# ! due to the __bool__ method, you can use the MaybeUser object as a boolean + +if not user: + print("Detected error") +""" +Detected error +""" diff --git a/examples/simple-extraction/user.py b/examples/simple-extraction/user.py new file mode 100644 index 000000000..8f09bef75 --- /dev/null +++ b/examples/simple-extraction/user.py @@ -0,0 +1,58 @@ +import instructor +import openai +from pydantic import BaseModel, Field +from typing import Optional + +instructor.patch() + + +class UserDetail(BaseModel): + age: int + name: str + role: Optional[str] = Field(default=None) + + +def get_user_detail(string) -> UserDetail: + return openai.ChatCompletion.create( + model="gpt-3.5-turbo-0613", + response_model=UserDetail, + messages=[ + { + "role": "user", + "content": f"Get user details for {string}", + }, + ], + ) # type: ignore + + +user = get_user_detail("Jason is 25 years old") +print(user.model_dump_json(indent=2)) +""" +{ + "age": 25, + "name": "Jason", + "role": null +} +""" + +user = get_user_detail("Jason is a 25 years old scientist") +print(user.model_dump_json(indent=2)) +""" +{ + "age": 25, + "name": "Jason", + "role": "scientist" +} +""" + +# ! notice that the string should not contain anything +# ! but a user and age was still extracted ?! +user = get_user_detail("User not found") +print(user.model_dump_json(indent=2)) +""" +{ + "age": 25, + "name": "John Doe", + "role": "null" +} +""" diff --git a/instructor/__init__.py b/instructor/__init__.py index cedb138ec..4a1761f50 100644 --- a/instructor/__init__.py +++ b/instructor/__init__.py @@ -1,11 +1,12 @@ from .function_calls import OpenAISchema, openai_function, openai_schema -from .dsl.multitask import MultiTask +from .dsl import MultiTask, Maybe from .patch import patch __all__ = [ "OpenAISchema", "openai_function", "MultiTask", + "Maybe", "openai_schema", "patch", ] diff --git a/instructor/dsl/__init__.py b/instructor/dsl/__init__.py index fa5fcca82..c48e73832 100644 --- a/instructor/dsl/__init__.py +++ b/instructor/dsl/__init__.py @@ -1,5 +1,6 @@ from .completion import ChatCompletion from .messages import * from .multitask import MultiTask +from .maybe import Maybe -__all__ = ["ChatCompletion", "MultiTask", "messages"] +__all__ = ["ChatCompletion", "MultiTask", "messages", "Maybe"] diff --git a/instructor/dsl/maybe.py b/instructor/dsl/maybe.py new file mode 100644 index 000000000..f3a80a7b2 --- /dev/null +++ b/instructor/dsl/maybe.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel, Field, create_model +from typing import Type, Optional + + +class MaybeBase(BaseModel): + result: Optional[BaseModel] + error: bool = Field(default=False) + message: Optional[str] + + def __bool__(self): + return self.result is not None # type: ignore + + +def Maybe(model: Type[BaseModel]) -> MaybeBase: + """ + Create a Maybe model for a given Pydantic model. + + Parameters: + model (Type[BaseModel]): The Pydantic model to wrap with Maybe. + + Returns: + MaybeModel (Type[BaseModel]): A new Pydantic model that includes fields for `result`, `error`, and `message`. + """ + + class MaybeBase(BaseModel): + def __bool__(self): + return self.result is not None # type: ignore + + fields = { + "result": ( + Optional[model], + Field( + default=None, + description="Correctly extracted result from the model, if any, otherwise None", + ), + ), + "error": (bool, Field(default=False)), + "message": ( + Optional[str], + Field( + default=None, + description="Error message if no result was found, should be short and concise", + ), + ), + } + + MaybeModel = create_model(f"Maybe{model.__name__}", __base__=MaybeBase, **fields) + + return MaybeModel diff --git a/mkdocs.yml b/mkdocs.yml index 1d1eb2cde..ac90fe428 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - Introduction: - Getting Started: 'index.md' - MultiTask: "multitask.md" + - Maybe: "maybe.md" - Philosophy: 'philosophy.md' - Use Cases: - 'Overview': 'examples/index.md'