c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Update Platform Components Table / update (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Docker image release / Build base image (push) Waiting to run
277 lines
11 KiB
Plaintext
277 lines
11 KiB
Plaintext
---
|
|
title: "Serializing Pipelines"
|
|
id: serialization
|
|
slug: "/serialization"
|
|
description: "Save your pipelines into a custom format and explore the serialization options."
|
|
---
|
|
|
|
# Serializing Pipelines
|
|
|
|
Save your pipelines into a custom format and explore the serialization options.
|
|
|
|
Serialization means converting a pipeline to a format that you can save on your disk and load later.
|
|
|
|
Haystack supports YAML format for pipeline serialization.
|
|
|
|
## Converting a Pipeline to YAML
|
|
|
|
Use the `dumps()` method to convert a Pipeline object to YAML:
|
|
|
|
```python
|
|
from haystack import Pipeline
|
|
|
|
pipe = Pipeline()
|
|
print(pipe.dumps())
|
|
|
|
# Prints:
|
|
#
|
|
# components: {}
|
|
# connections: []
|
|
# max_runs_per_component: 100
|
|
# metadata: {}
|
|
```
|
|
|
|
You can also use `dump()` method to save the YAML representation of a pipeline in a file:
|
|
|
|
```python
|
|
with open("/content/test.yml", "w") as file:
|
|
pipe.dump(file)
|
|
```
|
|
|
|
## Converting a Pipeline Back to Python
|
|
|
|
You can convert a YAML pipeline back into Python. Use the `loads()` method to convert a string representation of a pipeline (`str`, `bytes` or `bytearray`) or the `load()` method to convert a pipeline represented in a file-like object into a corresponding Python object.
|
|
|
|
Both loading methods support callbacks that let you modify components during the deserialization process. Deserialization is gated by a trusted-module allowlist, so pipelines referencing classes outside of it fail to load until you extend the allowlist — see [Deserialization Security](#deserialization-security) below.
|
|
|
|
Here is an example script:
|
|
|
|
```python
|
|
from haystack import Pipeline
|
|
from haystack.core.serialization import DeserializationCallbacks
|
|
from typing import Type, Dict, Any
|
|
|
|
# This is the YAML you want to convert to Python:
|
|
pipeline_yaml = """
|
|
components:
|
|
cleaner:
|
|
init_parameters:
|
|
remove_empty_lines: true
|
|
remove_extra_whitespaces: true
|
|
remove_regex: null
|
|
remove_repeated_substrings: false
|
|
remove_substrings: null
|
|
type: haystack.components.preprocessors.document_cleaner.DocumentCleaner
|
|
converter:
|
|
init_parameters:
|
|
encoding: utf-8
|
|
type: haystack.components.converters.txt.TextFileToDocument
|
|
connections:
|
|
- receiver: cleaner.documents
|
|
sender: converter.documents
|
|
max_runs_per_component: 100
|
|
metadata: {}
|
|
"""
|
|
|
|
|
|
def component_pre_init_callback(
|
|
component_name: str,
|
|
component_cls: Type,
|
|
init_params: Dict[str, Any],
|
|
):
|
|
# This function gets called every time a component is deserialized.
|
|
if component_name == "cleaner":
|
|
assert "DocumentCleaner" in component_cls.__name__
|
|
# Modify the init parameters. The modified parameters are passed to
|
|
# the init method of the component during deserialization.
|
|
init_params["remove_empty_lines"] = False
|
|
print("Modified 'remove_empty_lines' to False in 'cleaner' component")
|
|
else:
|
|
print(f"Not modifying component {component_name} of class {component_cls}")
|
|
|
|
|
|
pipe = Pipeline.loads(
|
|
pipeline_yaml,
|
|
callbacks=DeserializationCallbacks(component_pre_init_callback),
|
|
)
|
|
```
|
|
|
|
## Deserialization Security
|
|
|
|
Loading a pipeline instantiates the classes referenced in the serialized data. To prevent a crafted YAML file from importing and instantiating arbitrary classes, `Pipeline.load`, `Pipeline.loads`, and `Pipeline.from_dict` refuse to import classes from modules outside a trusted-module allowlist and raise a `DeserializationError` instead.
|
|
|
|
By default, the allowlist contains `haystack`, `haystack_integrations`, `haystack_experimental`, `builtins`, `typing`, and `collections`. Dangerous builtins such as `eval`, `exec`, `compile`, `__import__`, `open`, and `getattr` are blocked even though `builtins` is allowlisted.
|
|
|
|
### Allowing Custom Modules
|
|
|
|
Pipelines that reference custom components or callables in other packages fail to load until you add the modules to the allowlist. You can extend it in three ways:
|
|
|
|
```python
|
|
from haystack import Pipeline
|
|
|
|
# 1. Per call: pass additional module patterns for this deserialization only
|
|
pipe = Pipeline.load(open("pipeline.yaml"), allowed_modules=["mypkg.*"])
|
|
|
|
# 2. Process-wide: extend the allowlist programmatically
|
|
from haystack.core.serialization import allow_deserialization_module
|
|
|
|
allow_deserialization_module("mypkg")
|
|
```
|
|
|
|
```shell
|
|
# 3. Environment variable with comma-separated patterns, read on every deserialization call
|
|
export HAYSTACK_DESERIALIZATION_ALLOWLIST="mypkg.*,otherpkg.*"
|
|
```
|
|
|
|
Patterns are matched as prefixes by default (`"mypkg"` matches `mypkg` and any of its submodules), or as `fnmatch` globs if they contain `*`, `?`, or `[` somewhere other than a trailing `.*`. A trailing `.*` is treated as a prefix match, so `"mypkg"` and `"mypkg.*"` behave identically.
|
|
|
|
If the source of the serialized data is fully trusted, you can bypass the allowlist entirely with `unsafe=True`:
|
|
|
|
```python
|
|
pipe = Pipeline.load(open("pipeline.yaml"), unsafe=True)
|
|
```
|
|
|
|
Only use `unsafe=True` when you fully trust where the serialized pipeline comes from — it also lifts the block on dangerous builtins.
|
|
|
|
### Nested Init Parameter Validation
|
|
|
|
As an additional safeguard, deserialization validates the keys of `init_parameters` against the class's `__init__` signature before recursing into any nested `{"type": "...", "init_parameters": {...}}` dictionary. A nested dictionary whose key is not an accepted parameter name is rejected with a `DeserializationError` *before* the nested type is imported, which blocks attempts to smuggle untrusted classes into unused parameter slots. Classes whose constructor takes `**kwargs` are exempt, since their accepted parameter set cannot be statically determined.
|
|
|
|
This validation may surface pre-existing bugs in YAML files — for example typos, leftovers from renamed or removed parameters, or stale snapshots from older Haystack versions. The fix is to update the YAML so each nested-component key matches a real `__init__` parameter of the parent class.
|
|
|
|
## Default Serialization Behavior
|
|
|
|
The serialization system uses `default_to_dict` and `default_from_dict` to handle many object types automatically. You typically do **not** need to implement custom `to_dict`/`from_dict` for:
|
|
|
|
- **Secrets**: serialized and deserialized automatically so that sensitive values aren't stored in plain text.
|
|
- **ComponentDevice**: device configuration is detected and restored automatically.
|
|
- **Objects with their own `to_dict`/`from_dict`**: any init parameter whose type defines `to_dict()` is serialized by calling it; any dict in `init_parameters` with a `type` key pointing to a class with `from_dict()` is deserialized automatically.
|
|
|
|
To serialize or deserialize a single component, you can use `component_to_dict` and `component_from_dict` from `haystack.core.serialization`. They use the default behavior above as a fallback when the component doesn't define custom `to_dict`/`from_dict`:
|
|
|
|
```python
|
|
from haystack import component
|
|
from haystack.core.serialization import component_from_dict, component_to_dict
|
|
|
|
|
|
@component
|
|
class Greeter:
|
|
def __init__(self, message: str = "Hello"):
|
|
self.message = message
|
|
|
|
@component.output_types(greeting=str)
|
|
def run(self, name: str):
|
|
return {"greeting": f"{self.message}, {name}!"}
|
|
|
|
|
|
# Serialize a component instance to a dictionary
|
|
greeter = Greeter(message="Hi")
|
|
data = component_to_dict(greeter, "my_greeter")
|
|
|
|
# Deserialize back to a component instance
|
|
restored = component_from_dict(Greeter, data, "my_greeter")
|
|
assert restored.message == greeter.message
|
|
```
|
|
|
|
:::caution[Init parameters must be stored as instance attributes]
|
|
|
|
Default serialization only works when there is a **1:1 mapping** between init parameter names and instance attributes. For every argument in `__init__`, the component must assign it to an attribute with the same name. For example, if you have `def __init__(self, prompt: str)`, you must have `self.prompt = prompt` in the class. Otherwise the serialization logic can't find the value to serialize and raises an error or uses the default value if the parameter has one.
|
|
:::
|
|
|
|
## Performing Custom Serialization
|
|
|
|
Pipelines and components in Haystack can serialize simple components, including custom ones, out of the box. Code like this just works:
|
|
|
|
```python
|
|
from haystack import component
|
|
|
|
|
|
@component
|
|
class RepeatWordComponent:
|
|
def __init__(self, times: int):
|
|
self.times = times
|
|
|
|
@component.output_types(result=str)
|
|
def run(self, word: str):
|
|
return word * self.times
|
|
```
|
|
|
|
On the other hand, this code doesn't work if the final format is JSON, as the `set` type is not JSON-serializable:
|
|
|
|
```python
|
|
from haystack import component
|
|
|
|
|
|
@component
|
|
class SetIntersector:
|
|
def __init__(self, intersect_with: set):
|
|
self.intersect_with = intersect_with
|
|
|
|
@component.output_types(result=set)
|
|
def run(self, data: set):
|
|
return data.intersection(self.intersect_with)
|
|
```
|
|
|
|
In such cases, you can provide your own implementation `from_dict` and `to_dict` to components:
|
|
|
|
```python
|
|
from haystack import component, default_from_dict, default_to_dict
|
|
|
|
|
|
class SetIntersector:
|
|
def __init__(self, intersect_with: set):
|
|
self.intersect_with = intersect_with
|
|
|
|
@component.output_types(result=set)
|
|
def run(self, data: set):
|
|
return data.intersect(self.intersect_with)
|
|
|
|
def to_dict(self):
|
|
return default_to_dict(self, intersect_with=list(self.intersect_with))
|
|
|
|
@classmethod
|
|
def from_dict(cls, data):
|
|
# convert the set into a list for the dict representation,
|
|
# so it can be converted to JSON
|
|
data["intersect_with"] = set(data["intersect_with"])
|
|
return default_from_dict(cls, data)
|
|
```
|
|
|
|
## Saving a Pipeline to a Custom Format
|
|
|
|
Once a pipeline is available in its dictionary format, the last step of serialization is to convert that dictionary into a format you can store or send over the wire. Haystack supports YAML out of the box, but if you need a different format, you can write a custom Marshaller.
|
|
|
|
A `Marshaller` is a Python class responsible for converting text to a dictionary and a dictionary to text according to a certain format. Marshallers must respect the `Marshaller` [protocol](https://github.com/deepset-ai/haystack/blob/main/haystack/marshal/protocol.py), providing the methods `marshal` and `unmarshal`.
|
|
|
|
This is the code for a custom TOML marshaller that relies on the `rtoml` library:
|
|
|
|
```python
|
|
# This code requires a `pip install rtoml`
|
|
from typing import Dict, Any, Union
|
|
import rtoml
|
|
|
|
|
|
class TomlMarshaller:
|
|
def marshal(self, dict_: Dict[str, Any]) -> str:
|
|
return rtoml.dumps(dict_)
|
|
|
|
def unmarshal(self, data_: Union[str, bytes]) -> Dict[str, Any]:
|
|
return dict(rtoml.loads(data_))
|
|
```
|
|
|
|
You can then pass a Marshaller instance to the methods `dump`, `dumps`, `load`, and `loads`:
|
|
|
|
```python
|
|
from haystack import Pipeline
|
|
from my_custom_marshallers import TomlMarshaller
|
|
|
|
pipe = Pipeline()
|
|
pipe.dumps(TomlMarshaller())
|
|
# prints:
|
|
# 'max_runs_per_component = 100\nconnections = []\n\n[metadata]\n\n[components]\n'
|
|
```
|
|
|
|
## Additional References
|
|
|
|
:notebook: Tutorial: [Serializing LLM Pipelines](https://haystack.deepset.ai/tutorials/29_serializing_pipelines)
|