Skip to content

Commit

Permalink
Integrate OpenAI API Structured Generation
Browse files Browse the repository at this point in the history
  • Loading branch information
lapp0 committed Sep 14, 2024
1 parent 0b9a3f1 commit 1e2e389
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 28 deletions.
2 changes: 1 addition & 1 deletion docs/overrides/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,4 @@ <h2 class="subtitle" style="font-weight: 400; padding-top: 1rem;">
</section>
{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
{% block footer %}{% endblock %}
1 change: 0 additions & 1 deletion docs/overrides/main.html
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
{% extends "base.html" %}

48 changes: 39 additions & 9 deletions docs/reference/models/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,25 @@

## OpenAI models

Outlines supports models available via the OpenAI Chat API, e.g. ChatGPT and GPT-4. You can initialize the model by passing the model name to `outlines.models.openai`:
Outlines supports models available via the OpenAI Chat API, e.g. GPT-4o, ChatGPT and GPT-4. You can initialize the model by passing the model name to `outlines.models.openai`:

```python
from outlines import models


model = models.openai("gpt-3.5-turbo")
model = models.openai("gpt-4-turbo")
model = models.openai("gpt-4o-mini")
model = models.openai("gpt-4o")
```

Check the [OpenAI documentation](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4) for an up-to-date list of available models. You can pass any parameter you would pass to `openai.AsyncOpenAI` as keyword arguments:
Check the [OpenAI documentation](https://platform.openai.com/docs/models/gpt-4o) for an up-to-date list of available models. You can pass any parameter you would pass to `openai.AsyncOpenAI` as keyword arguments:

```python
import os
from outlines import models


model = models.openai(
"gpt-3.5-turbo",
"gpt-4o-mini",
api_key=os.environ["OPENAI_API_KEY"]
)
```
Expand Down Expand Up @@ -56,8 +55,8 @@ from outlines import models

model = models.azure_openai(
"azure-deployment-name",
"gpt-3.5-turbo",
api_version="2023-07-01-preview",
"gpt-4o-mini",
api_version="2024-07-18",
azure_endpoint="https://example-endpoint.openai.azure.com",
)
```
Expand Down Expand Up @@ -111,6 +110,37 @@ model = models.openai(client, config)

You need to pass the async client to be able to do batch inference.

## Structured Generation Support

Outlines provides support for [OpenAI Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs/json-mode) via `outlines.generate.json`, `outlines.generate.choice`

```python
from pydantic import BaseModel, ConfigDict
import outlines.models as models
from outlines import generate

model = models.openai("gpt-4o-mini")

class Person(BaseModel):
model_config = ConfigDict(extra='forbid') # required for openai
first_name: str
last_name: str
age: int

generate.json(model, Person)
generator("current indian prime minister on january 1st 2023")
# Person(first_name='Narendra', last_name='Modi', age=72)

generator = generate.choice(model, ["Chicken", "Egg"])
print(generator("Which came first?"))
# Chicken
```

!!! Warning

Structured generation support only provided to OpenAI-compatible endpoints which conform to OpenAI's standard. Additionally, `generate.regex` and `generate.cfg` are not supported.


## Advanced configuration

For more advanced configuration option, such as support proxy, please consult the [OpenAI SDK's documentation](https://github.com/openai/openai-python):
Expand Down Expand Up @@ -146,7 +176,7 @@ config = OpenAIConfig(
top_p=.95,
seed=0,
)
model = models.openai("gpt-3.5-turbo", config)
model = models.openai("gpt-4o-mini", config)
```

## Monitoring API use
Expand All @@ -158,7 +188,7 @@ from openai import AsyncOpenAI
import outlines.models


model = models.openai("gpt-4")
model = models.openai("gpt-4o")

print(model.prompt_tokens)
# 0
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Outlines provides a unified interface to generate text with many language models
```python
from outlines import models, generate

model = models.openai("gpt-4")
model = models.openai("gpt-4o-mini")
generator = generate.text(model)
answer = generator("What is 2+2?")

Expand Down
27 changes: 19 additions & 8 deletions outlines/generate/choice.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json as pyjson
from functools import singledispatch
from typing import Callable, List

from outlines.generate.api import SequenceGeneratorAdapter
from outlines.models import OpenAI
from outlines.samplers import Sampler, multinomial

from .json import json
from .regex import regex


Expand All @@ -24,13 +26,22 @@ def choice(
def choice_openai(
model: OpenAI, choices: List[str], sampler: Sampler = multinomial()
) -> Callable:
if not isinstance(sampler, multinomial):
raise NotImplementedError(
r"The OpenAI API does not support any other sampling algorithm "
+ "that the multinomial sampler."
)

def generate_choice(prompt: str, max_tokens: int = 1):
return model.generate_choice(prompt, choices, max_tokens)
"""
Call OpenAI API with response_format of a dict:
{"result": <one of choices>}
"""

choices_schema = pyjson.dumps(
{
"type": "object",
"properties": {"result": {"type": "string", "enum": choices}},
"additionalProperties": False,
"required": ["result"],
}
)
generator = json(model, choices_schema, sampler)

def generate_choice(*args, **kwargs):
return generator(*args, **kwargs)["result"]

return generate_choice
48 changes: 45 additions & 3 deletions outlines/generate/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,49 @@ def json(
def json_openai(
model, schema_object: Union[str, object, Callable], sampler: Sampler = multinomial()
):
raise NotImplementedError(
"Cannot use JSON Schema-structure generation with an OpenAI model "
+ "due to the limitations of the OpenAI API"
if not isinstance(sampler, multinomial):
raise NotImplementedError(
r"The OpenAI API does not support any other sampling algorithm "
+ "than the multinomial sampler."
)

if isinstance(schema_object, type(BaseModel)):
schema = pyjson.dumps(schema_object.model_json_schema())
elif callable(schema_object):
schema = pyjson.dumps(get_schema_from_signature(schema_object))
elif isinstance(schema_object, str):
schema = schema_object
else:
raise ValueError(
f"Cannot parse schema {schema_object}. The schema must be either "
+ "a Pydantic object, a function or a string that contains the JSON "
+ "Schema specification"
)

# create copied, patched model with normalized json schema set
generator = model.new_with_replacements(
response_format={
"type": "json_schema",
"json_schema": {
"name": "default",
"strict": True,
"schema": pyjson.loads(schema),
},
}
)

# set generators sequence post-processor
if isinstance(schema_object, type(BaseModel)):
generator.format_sequence = lambda x: schema_object.parse_raw(x)
elif callable(schema_object):
generator.format_sequence = lambda x: pyjson.loads(x)
elif isinstance(schema_object, str) or isinstance(schema_object, dict):
generator.format_sequence = lambda x: pyjson.loads(x)
else:
raise ValueError(
f"Cannot parse schema {schema_object}. The schema must be either "
+ "a Pydantic object, a function or a string that contains the JSON "
+ "Schema specification"
)

return generator
13 changes: 8 additions & 5 deletions outlines/models/openai.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Integration with OpenAI's API."""
import copy
import functools
import warnings
from dataclasses import asdict, dataclass, field, replace
Expand Down Expand Up @@ -91,7 +92,6 @@ def __init__(
parameters that cannot be set by calling this class' methods.
tokenizer
The tokenizer associated with the model the client connects to.
"""

self.client = client
Expand All @@ -104,6 +104,8 @@ def __init__(
self.prompt_tokens = 0
self.completion_tokens = 0

self.format_sequence = lambda seq: seq

def __call__(
self,
prompt: Union[str, List[str]],
Expand Down Expand Up @@ -152,7 +154,7 @@ def __call__(
self.prompt_tokens += prompt_tokens
self.completion_tokens += completion_tokens

return response
return self.format_sequence(response)

def stream(self, *args, **kwargs):
raise NotImplementedError(
Expand Down Expand Up @@ -250,9 +252,10 @@ def generate_choice(

return choice

def generate_json(self):
"""Call the OpenAI API to generate a JSON object."""
raise NotImplementedError
def new_with_replacements(self, **kwargs):
new_instance = copy.copy(self)
new_instance.config = replace(new_instance.config, **kwargs)
return new_instance

def __str__(self):
return self.__class__.__name__ + " API"
Expand Down

0 comments on commit 1e2e389

Please sign in to comment.