Skip to content

Commit

Permalink
Add support for custom template types to be provided by plugins
Browse files Browse the repository at this point in the history
- Enhanced the Template class to allow dynamic retrieval of template classes based on type.
- Introduced a new hook for registering additional template types.
- Updated the load_template function to handle custom template types and raise appropriate errors for unknown types.
  • Loading branch information
btucker committed Jan 2, 2025
1 parent 000e984 commit 676ff74
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 20 deletions.
29 changes: 29 additions & 0 deletions docs/plugins/plugin-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,32 @@ This demonstrates how to register a model with both sync and async versions, and

The {ref}`model plugin tutorial <tutorial-model-plugin>` describes how to use this hook in detail. Asynchronous models {ref}`are described here <advanced-model-plugins-async>`.

(register-template-types)=
## register_template_types()

This hook allows plugins to register custom template types that can be used in prompt templates.

```python
from llm import Template, hookimpl

class CustomTemplate(Template):
type: str = "custom"

def evaluate(self, input: str, params=None):
# Custom processing here
prompt, system = super().evaluate(input, params)
return f"CUSTOM: {prompt}", system

def stringify(self):
# Custom string representation for llm templates list
return f"custom template: {self.prompt}"

@hookimpl
def register_template_types():
return {
"custom": CustomTemplate
}
```

Custom template types can modify how prompts are processed and how they appear in template listings. See {ref}`custom-template-types` for more details.

49 changes: 49 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,52 @@ Example:
llm -t roast 'How are you today?'
```
> I'm doing great but with your boring questions, I must admit, I've seen more life in a cemetery.

(custom-template-types)=
### Custom template types

Plugins can register custom template types that provide additional functionality. These templates are identified by a `type:` key in their YAML configuration.

For example, a plugin might provide a custom template type that adds special formatting or processing to the prompts:

```yaml
type: custom
prompt: Hello $input
system: Be helpful
```

Custom template types can customize how they appear in the template list by implementing a `stringify` method. This allows them to provide a more descriptive or formatted representation of their configuration when users run `llm templates list`.

To create a custom template type in a plugin:

1. Create a class that inherits from `Template`
2. Set a `type` attribute to identify your template type
3. Override methods like `evaluate` to customize behavior
4. Optionally implement `stringify` to control how the template appears in listings
5. Register your template type using the `register_template_types` hook

For details on implementing the plugin hook, see {ref}`register_template_types() <register-template-types>`.

Example plugin implementation:

```python
from llm import Template, hookimpl
class CustomTemplate(Template):
type: str = "custom"
def evaluate(self, input: str, params=None):
# Custom processing here
prompt, system = super().evaluate(input, params)
return f"CUSTOM: {prompt}", system
def stringify(self):
# Custom string representation
return f"custom template: {self.prompt}"
@hookimpl
def register_template_types():
return {
"custom": CustomTemplate
}
```
23 changes: 15 additions & 8 deletions llm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,14 +1155,18 @@ def templates_list():
for file in path.glob("*.yaml"):
name = file.stem
template = load_template(name)
text = []
if template.system:
text.append(f"system: {template.system}")
if template.prompt:
text.append(f" prompt: {template.prompt}")
if hasattr(template, "stringify"):
text = template.stringify()
else:
text = [template.prompt if template.prompt else ""]
pairs.append((name, "".join(text).replace("\n", " ")))
text = []
if template.system:
text.append(f"system: {template.system}")
if template.prompt:
text.append(f" prompt: {template.prompt}")
else:
text = [template.prompt if template.prompt else ""]
text = "".join(text)
pairs.append((name, text.replace("\n", " ")))
try:
max_name_len = max(len(p[0]) for p in pairs)
except ValueError:
Expand Down Expand Up @@ -1875,11 +1879,14 @@ def load_template(name):
return Template(name=name, prompt=loaded)
loaded["name"] = name
try:
return Template(**loaded)
template_class = Template.get_template_class(loaded.get("type"))
return template_class(**loaded)
except pydantic.ValidationError as ex:
msg = "A validation error occurred:\n"
msg += render_errors(ex.errors())
raise click.ClickException(msg)
except ValueError as ex:
raise click.ClickException(str(ex))


def get_history(chat_id):
Expand Down
9 changes: 9 additions & 0 deletions llm/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ def register_models(register):
@hookspec
def register_embedding_models(register):
"Register additional model instances that can be used for embedding"


@hookspec
def register_template_types():
"""Register additional template types that can be used for prompt templates.
Returns:
dict: A dictionary mapping template type names to template classes
"""
23 changes: 21 additions & 2 deletions llm/templates.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from pydantic import BaseModel
import string
from typing import Optional, Any, Dict, List, Tuple
from typing import Optional, Any, Dict, List, Tuple, Type, Literal
from .plugins import pm


class Template(BaseModel):
name: str
type: Optional[str] = None
prompt: Optional[str] = None
system: Optional[str] = None
model: Optional[str] = None
Expand All @@ -13,11 +15,28 @@ class Template(BaseModel):
extract: Optional[bool] = None

class Config:
extra = "forbid"
extra = "allow"

class MissingVariables(Exception):
pass

@classmethod
def get_template_class(cls, type: Optional[str]) -> Type["Template"]:
"""Get the template class for a given type."""
if not type:
return cls

# Get registered template types from plugins
template_types = {}
for hook_result in pm.hook.register_template_types():
if hook_result:
template_types.update(hook_result)

if type not in template_types:
raise ValueError(f"Unknown template type: {type}")

return template_types[type]

def evaluate(
self, input: str, params: Optional[Dict[str, Any]] = None
) -> Tuple[Optional[str], Optional[str]]:
Expand Down
Loading

0 comments on commit 676ff74

Please sign in to comment.