Skip to content

Commit

Permalink
implement maybe
Browse files Browse the repository at this point in the history
  • Loading branch information
jxnl committed Sep 7, 2023
1 parent c825b23 commit 4403b49
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 3 deletions.
32 changes: 32 additions & 0 deletions docs/maybe.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 35 additions & 1 deletion docs/tips/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.")
```
```

77 changes: 77 additions & 0 deletions examples/simple-extraction/maybe_user.py
Original file line number Diff line number Diff line change
@@ -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
"""
58 changes: 58 additions & 0 deletions examples/simple-extraction/user.py
Original file line number Diff line number Diff line change
@@ -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"
}
"""
3 changes: 2 additions & 1 deletion instructor/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
3 changes: 2 additions & 1 deletion instructor/dsl/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
49 changes: 49 additions & 0 deletions instructor/dsl/maybe.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 4403b49

Please sign in to comment.