Coverage for haystack/hooks/tool_result_offloading/stores.py: 100%
27 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from pathlib import Path
6from typing import Any
8from haystack.core.serialization import default_from_dict, default_to_dict
9from haystack.hooks.tool_result_offloading.types import ToolResultStore
12class FileSystemToolResultStore(ToolResultStore):
13 """
14 A `ToolResultStore` that writes offloaded tool results to files under a root directory on the local file system.
16 ```python
17 from haystack.hooks.tool_result_offloading import FileSystemToolResultStore
19 store = FileSystemToolResultStore(root="tool_results")
20 reference = store.write(key="search_1.txt", content="...")
21 store.read(reference)
22 ```
23 """
25 def __init__(self, root: str | Path) -> None:
26 """
27 Initialize the store with the root directory results are written under.
29 :param root: Directory under which result files are written. Created on first write if it does not exist.
30 """
31 self.root = Path(root)
33 def _resolve_in_root(self, path_like: str | Path, *, subject: str) -> Path:
34 """
35 Resolve a path-like value and ensure it stays within the configured store root.
37 Relative values are interpreted relative to `self.root`; absolute values are used as-is.
39 :param path_like: Relative or absolute path-like value to resolve.
40 :param subject: Human-readable label used in the error message.
41 :returns: The resolved absolute path within the store root.
42 :raises ValueError: If the resolved path escapes the store root.
43 """
44 root = self.root.resolve()
45 path = Path(path_like)
46 candidate = path if path.is_absolute() else root / path
47 resolved = candidate.resolve()
48 if not resolved.is_relative_to(root):
49 raise ValueError(f"{subject} '{path_like}' resolves outside the store root '{root}'.")
50 return resolved
52 def write(self, *, key: str, content: str) -> str:
53 """
54 Write `content` to `<root>/<key>`, creating parent directories, and return the file path.
56 The resolved target must stay within the root directory: a `key` that escapes it (e.g. containing `../` or an
57 absolute path) is rejected, so a tool-provided key cannot write outside the store.
59 :param key: Relative file name for the result within the store root.
60 :param content: The tool result to persist.
61 :returns: The absolute path the content was written to, as a string, for use with `read`.
62 :raises ValueError: If `key` resolves to a location outside the store root.
63 """
64 path = self._resolve_in_root(key, subject="Result key")
65 path.parent.mkdir(parents=True, exist_ok=True)
66 path.write_text(content, encoding="utf-8")
67 return str(path)
69 def read(self, reference: str) -> str:
70 """
71 Read back the content previously written to `reference`.
73 The resolved reference must stay within the store root: callers must treat it as an opaque
74 store-scoped reference, not as an arbitrary filesystem path.
76 :param reference: A store reference returned by `write`.
77 :returns: The stored content.
78 :raises ValueError: If `reference` resolves to a location outside the store root.
79 """
80 return self._resolve_in_root(reference, subject="Result reference").read_text(encoding="utf-8")
82 def to_dict(self) -> dict[str, Any]:
83 """
84 Serialize the store, storing its root directory as a string.
86 :returns: A dictionary representation of the store.
87 """
88 return default_to_dict(self, root=str(self.root))
90 @classmethod
91 def from_dict(cls, data: dict[str, Any]) -> "FileSystemToolResultStore":
92 """
93 Deserialize the store from a dictionary.
95 :param data: A dictionary representation produced by `to_dict`.
96 :returns: The deserialized `FileSystemToolResultStore`.
97 """
98 return default_from_dict(cls, data)