Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename Prompt to Template and add deprecation warning to the prompt decorator #1440

Merged
merged 5 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 29 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,12 @@ First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/la

## Features

- [x] 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba
- [x] 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/)
- [x] 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping
- [x] ⚡ Fast [regex-structured generation](#efficient-regex-structured-generation)
- [x] 🔥 Fast [JSON generation](#efficient-json-generation-following-a-pydantic-model) following a JSON schema or a Pydantic model
- [x] 📝 [Grammar-structured generation](#using-context-free-grammars-to-guide-generation)
- [x] 🐍 Interleave completions with loops, conditionals, and custom Python functions
- [x] 💾 Caching of generations
- [x] 🗂️ Batch inference
- [x] 🎲 Sample with the greedy, multinomial and beam search algorithms (and more to come!)
- [x] 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)!
- 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba
- 🔥 Fast [JSON generation](#efficient-json-generation-following-a-pydantic-model) following a JSON schema or a Pydantic model
- 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping
- 📝 Generate text that follows a [regex](#efficient-regex-structured-generation) or a [context-free grammar](#using-context-free-grammars-to-guide-generation)
- 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/)
- 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)!


Outlines has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date!
Expand Down Expand Up @@ -379,13 +374,23 @@ print(result)
## Prompting

Building prompts can get messy. **Outlines** makes it easier to write and manage
prompts by encapsulating templates inside "template functions".
prompts by encapsulating templates inside "template functions". Template
functions use the Jinja2 templating engine to help build complex prompts in a
concise manner.

These functions make it possible to neatly separate the prompt logic from the
general program logic; they can be imported from other modules and libraries.
Template functions are created by loading a Jinja2 template from a text file.
Assume you have the following prompt template defined in `prompt.txt`:

Template functions require no superfluous abstraction, they use the Jinja2
templating engine to help build complex prompts in a concise manner:
``` text
You are a sentiment-labelling assistant.

{% for example in examples %}
{{ example[0] }} // {{ example[1] }}
{% endfor %}
{{ to_label }} //
```

You can then load it and call it with:

``` python
import outlines
Expand All @@ -397,21 +402,17 @@ examples = [
("The waiter was rude", "Negative")
]

@outlines.prompt
def labelling(to_label, examples):
"""You are a sentiment-labelling assistant.

{% for example in examples %}
{{ example[0] }} // {{ example[1] }}
{% endfor %}
{{ to_label }} //
"""

model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
labelling = outlines.Template.from_file("prompt.txt")
prompt = labelling("Just awesome", examples)
answer = outlines.generate.text(model)(prompt, max_tokens=100)
```

This helps:

- Keep content separate from the code
- Design "white space perfect" prompts

It is more maintainable and means prompts can be versioned separately from the code.

## Join us

- 💡 **Have an idea?** Come chat with us on [Discord][discord]
Expand Down
1 change: 0 additions & 1 deletion docs/api/prompts.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/api/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: outlines.templates
2 changes: 1 addition & 1 deletion outlines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from outlines.base import vectorize
from outlines.caching import clear_cache, disable_cache, get_cache
from outlines.function import Function
from outlines.prompts import Prompt, prompt
from outlines.templates import Template, prompt

__all__ = [
"clear_cache",
Expand Down
2 changes: 1 addition & 1 deletion outlines/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def get_cache():
"""
from outlines._version import __version__ as outlines_version # type: ignore

outlines_cache_dir = os.environ.get('OUTLINES_CACHE_DIR')
outlines_cache_dir = os.environ.get("OUTLINES_CACHE_DIR")
xdg_cache_home = os.environ.get("XDG_CACHE_HOME")
home_dir = os.path.normpath(os.path.expanduser("~"))
if outlines_cache_dir:
Expand Down
9 changes: 6 additions & 3 deletions outlines/fsm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

from outlines.types import Regex, boolean as boolean_regex, date as date_regex
from outlines.types import datetime as datetime_regex
from outlines.types import integer as integer_regex, number as number_regex, time as time_regex
from outlines.types import (
integer as integer_regex,
number as number_regex,
time as time_regex,
)


class FormatFunction(Protocol):
def __call__(self, sequence: str) -> Any:
...
def __call__(self, sequence: str) -> Any: ...


def python_types_to_regex(python_type: Type) -> Tuple[Regex, FormatFunction]:
Expand Down
4 changes: 2 additions & 2 deletions outlines/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

if TYPE_CHECKING:
from outlines.generate.api import SequenceGenerator
from outlines.prompts import Prompt
from outlines.templates import Template


@dataclass
Expand All @@ -22,7 +22,7 @@ class Function:

"""

prompt_template: "Prompt"
prompt_template: "Template"
schema: Union[str, Callable, object]
model_name: str
generator: Optional["SequenceGenerator"] = None
Expand Down
9 changes: 8 additions & 1 deletion 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
from dataclasses import asdict, dataclass, field, replace
Expand Down Expand Up @@ -139,7 +140,13 @@ def __call__(
if samples is None:
samples = self.config.n

config = replace(self.config, max_tokens=max_tokens, temperature=temperature, n=samples, stop=stop_at) # type: ignore
config = replace(
self.config,
max_tokens=max_tokens,
temperature=temperature,
n=samples,
stop=stop_at,
) # type: ignore

response, prompt_tokens, completion_tokens = generate_chat(
prompt, system_prompt, self.client, config
Expand Down
1 change: 1 addition & 0 deletions outlines/processors/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
See the License for the specific language governing permissions and
limitations under the License.
"""

import math
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union

Expand Down
3 changes: 1 addition & 2 deletions outlines/samplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ def __call__(
next_token_logits: "torch.DoubleTensor",
sequence_weights: "torch.DoubleTensor",
rng: "torch.Generator",
) -> "torch.DoubleTensor":
...
) -> "torch.DoubleTensor": ...


@dataclass(frozen=True)
Expand Down
3 changes: 2 additions & 1 deletion outlines/serve/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ async def generate(request: Request) -> Response:
logits_processors = []

sampling_params = SamplingParams(
**request_dict, logits_processors=logits_processors # type: ignore
**request_dict,
logits_processors=logits_processors, # type: ignore
)
request_id = random_uuid()

Expand Down
131 changes: 82 additions & 49 deletions outlines/prompts.py → outlines/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Type, cast
import warnings

import jinja2
import pydantic


@dataclass
class Prompt:
"""Represents a prompt function.
class Template:
"""Represents a prompt template.

We return a `Prompt` class instead of a simple function so the
template defined in prompt functions can be accessed.
We return a `Template` class instead of a simple function so the
template can be accessed by callers.

"""

Expand All @@ -41,8 +42,7 @@ def __call__(self, *args, **kwargs) -> str:

@classmethod
def from_str(cls, content: str, filters: Dict[str, Callable] = {}):
"""
Create an instance of the class from a string.
"""Create a `Template` instance from a string containing a Jinja template.

Parameters
----------
Expand All @@ -53,12 +53,11 @@ def from_str(cls, content: str, filters: Dict[str, Callable] = {}):
-------
An instance of the class with the provided content as a template.
"""
return cls(cls._template_from_str(content, filters), None)
return cls(build_template_from_str(content, filters), None)

@classmethod
def from_file(cls, path: Path, filters: Dict[str, Callable] = {}):
"""
Create a Prompt instance from a file containing a Jinja template.
"""Create a `Template` instance from a file containing a Jinja template.

Note: This method does not allow to include and inheritance to reference files
that are outside the folder or subfolders of the file given to `from_file`.
Expand All @@ -70,49 +69,43 @@ def from_file(cls, path: Path, filters: Dict[str, Callable] = {}):

Returns
-------
Prompt
An instance of the Prompt class with the template loaded from the file.
Template
An instance of the Template class with the template loaded from the file.
"""
# We don't use a `Signature` here because it seems not feasible to infer one from a Jinja2 environment that is
# split across multiple files (since e.g. we support features like Jinja2 includes and template inheritance)
return cls(cls._template_from_file(path, filters), None)
return cls(build_template_from_file(path, filters), None)

@classmethod
def _template_from_str(
_, content: str, filters: Dict[str, Callable] = {}
) -> jinja2.Template:
# Dedent, and remove extra linebreak
cleaned_template = inspect.cleandoc(content)

# Add linebreak if there were any extra linebreaks that
# `cleandoc` would have removed
ends_with_linebreak = content.replace(" ", "").endswith("\n\n")
if ends_with_linebreak:
cleaned_template += "\n"

# Remove extra whitespaces, except those that immediately follow a newline symbol.
# This is necessary to avoid introducing whitespaces after backslash `\` characters
# used to continue to the next line without linebreak.
cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template)

env = create_jinja_env(None, filters)
env.filters["name"] = get_fn_name
env.filters["description"] = get_fn_description
env.filters["source"] = get_fn_source
env.filters["signature"] = get_fn_signature
env.filters["schema"] = get_schema
env.filters["args"] = get_fn_args

return env.from_string(cleaned_template)

@classmethod
def _template_from_file(
_, path: Path, filters: Dict[str, Callable] = {}
) -> jinja2.Template:
file_directory = os.path.dirname(os.path.abspath(path))
env = create_jinja_env(jinja2.FileSystemLoader(file_directory), filters)
def build_template_from_str(
content: str, filters: Dict[str, Callable] = {}
) -> jinja2.Template:
# Dedent, and remove extra linebreak
cleaned_template = inspect.cleandoc(content)

# Add linebreak if there were any extra linebreaks that
# `cleandoc` would have removed
ends_with_linebreak = content.replace(" ", "").endswith("\n\n")
if ends_with_linebreak:
cleaned_template += "\n"

# Remove extra whitespaces, except those that immediately follow a newline symbol.
# This is necessary to avoid introducing whitespaces after backslash `\` characters
# used to continue to the next line without linebreak.
cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template)

env = create_jinja_env(None, filters)

return env.from_string(cleaned_template)

return env.get_template(os.path.basename(path))

def build_template_from_file(
path: Path, filters: Dict[str, Callable] = {}
) -> jinja2.Template:
file_directory = os.path.dirname(os.path.abspath(path))
env = create_jinja_env(jinja2.FileSystemLoader(file_directory), filters)

return env.get_template(os.path.basename(path))


def prompt(
Expand Down Expand Up @@ -170,9 +163,19 @@ def prompt(

Returns
-------
A `Prompt` callable class which will render the template when called.
A `Template` callable class which will render the template when called.

"""
warnings.warn(
"The @prompt decorator is deprecated and will be removed in outlines 1.1.0. "
"Instead of using docstring templates, please use Template.from_file() to "
"load your prompts from separate template files, or a simple Python function "
"that returns text. This helps keep prompt content separate from code and is "
"more maintainable.",
DeprecationWarning,
stacklevel=2,
)

if fn is None:
return lambda fn: prompt(fn, cast(Dict[str, Callable], filters))

Expand All @@ -184,14 +187,35 @@ def prompt(
if docstring is None:
raise TypeError("Could not find a template in the function's docstring.")

template = Prompt._template_from_str(cast(str, docstring), filters)
template = build_template_from_str(cast(str, docstring), filters)

return Prompt(template, signature)
return Template(template, signature)


def create_jinja_env(
loader: Optional[jinja2.BaseLoader], filters: Dict[str, Callable]
) -> jinja2.Environment:
"""Create a new Jinja environment.

The Jinja environment is loaded with a set of pre-defined filters:
- `name`: get the name of a function
- `description`: get a function's docstring
- `source`: get a function's source code
- `signature`: get a function's signature
- `args`: get a function's arguments
- `schema`: isplay a JSON Schema

Users may pass additional filters, and/or override existing ones.

Arguments
---------
loader
An optional `BaseLoader` instance
filters
A dictionary of filters, map between the filter's name and the
corresponding function.

"""
env = jinja2.Environment(
loader=loader,
trim_blocks=True,
Expand All @@ -200,6 +224,15 @@ def create_jinja_env(
undefined=jinja2.StrictUndefined,
)

env.filters["name"] = get_fn_name
env.filters["description"] = get_fn_description
env.filters["source"] = get_fn_source
env.filters["signature"] = get_fn_signature
env.filters["schema"] = get_schema
env.filters["args"] = get_fn_args

# The filters passed by the user may override the
# pre-defined filters.
for name, filter_fn in filters.items():
env.filters[name] = filter_fn

Expand Down
Loading
Loading