banks 2.1.3__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {banks-2.1.3 → banks-2.2.0}/PKG-INFO +1 -1
  2. {banks-2.1.3 → banks-2.2.0}/docs/examples.md +52 -0
  3. {banks-2.1.3 → banks-2.2.0}/src/banks/__about__.py +1 -1
  4. {banks-2.1.3 → banks-2.2.0}/src/banks/env.py +2 -1
  5. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/__init__.py +2 -1
  6. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/image.py +11 -1
  7. banks-2.2.0/src/banks/filters/xml.py +62 -0
  8. {banks-2.1.3 → banks-2.2.0}/tests/test_image.py +17 -0
  9. banks-2.2.0/tests/test_xml.py +70 -0
  10. {banks-2.1.3 → banks-2.2.0}/.github/workflows/docs.yml +0 -0
  11. {banks-2.1.3 → banks-2.2.0}/.github/workflows/release.yml +0 -0
  12. {banks-2.1.3 → banks-2.2.0}/.github/workflows/test.yml +0 -0
  13. {banks-2.1.3 → banks-2.2.0}/.gitignore +0 -0
  14. {banks-2.1.3 → banks-2.2.0}/CITATION.cff +0 -0
  15. {banks-2.1.3 → banks-2.2.0}/CLAUDE.md +0 -0
  16. {banks-2.1.3 → banks-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  17. {banks-2.1.3 → banks-2.2.0}/CONTRIBUTING.md +0 -0
  18. {banks-2.1.3 → banks-2.2.0}/LICENSE.txt +0 -0
  19. {banks-2.1.3 → banks-2.2.0}/MANIFEST.in +0 -0
  20. {banks-2.1.3 → banks-2.2.0}/README.md +0 -0
  21. {banks-2.1.3 → banks-2.2.0}/assets/banks.png +0 -0
  22. {banks-2.1.3 → banks-2.2.0}/cookbook/Prompt_Caching_with_Anthropic.ipynb +0 -0
  23. {banks-2.1.3 → banks-2.2.0}/cookbook/Prompt_Versioning.ipynb +0 -0
  24. {banks-2.1.3 → banks-2.2.0}/cookbook/in_prompt_completion.ipynb +0 -0
  25. {banks-2.1.3 → banks-2.2.0}/docs/config.md +0 -0
  26. {banks-2.1.3 → banks-2.2.0}/docs/index.md +0 -0
  27. {banks-2.1.3 → banks-2.2.0}/docs/prompt.md +0 -0
  28. {banks-2.1.3 → banks-2.2.0}/docs/python.md +0 -0
  29. {banks-2.1.3 → banks-2.2.0}/docs/registry.md +0 -0
  30. {banks-2.1.3 → banks-2.2.0}/mkdocs.yml +0 -0
  31. {banks-2.1.3 → banks-2.2.0}/pyproject.toml +0 -0
  32. {banks-2.1.3 → banks-2.2.0}/src/banks/__init__.py +0 -0
  33. {banks-2.1.3 → banks-2.2.0}/src/banks/cache.py +0 -0
  34. {banks-2.1.3 → banks-2.2.0}/src/banks/config.py +0 -0
  35. {banks-2.1.3 → banks-2.2.0}/src/banks/errors.py +0 -0
  36. {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/__init__.py +0 -0
  37. {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/chat.py +0 -0
  38. {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/completion.py +0 -0
  39. {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/docs.py +0 -0
  40. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/audio.py +0 -0
  41. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/cache_control.py +0 -0
  42. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/lemmatize.py +0 -0
  43. {banks-2.1.3 → banks-2.2.0}/src/banks/filters/tool.py +0 -0
  44. {banks-2.1.3 → banks-2.2.0}/src/banks/prompt.py +0 -0
  45. {banks-2.1.3 → banks-2.2.0}/src/banks/registries/__init__.py +0 -0
  46. {banks-2.1.3 → banks-2.2.0}/src/banks/registries/directory.py +0 -0
  47. {banks-2.1.3 → banks-2.2.0}/src/banks/registries/file.py +0 -0
  48. {banks-2.1.3 → banks-2.2.0}/src/banks/registries/redis.py +0 -0
  49. {banks-2.1.3 → banks-2.2.0}/src/banks/types.py +0 -0
  50. {banks-2.1.3 → banks-2.2.0}/src/banks/utils.py +0 -0
  51. {banks-2.1.3 → banks-2.2.0}/tests/__init__.py +0 -0
  52. {banks-2.1.3 → banks-2.2.0}/tests/conftest.py +0 -0
  53. {banks-2.1.3 → banks-2.2.0}/tests/data/1x1.png +0 -0
  54. {banks-2.1.3 → banks-2.2.0}/tests/data/empty.wav +0 -0
  55. {banks-2.1.3 → banks-2.2.0}/tests/e2e/__init__.py +0 -0
  56. {banks-2.1.3 → banks-2.2.0}/tests/e2e/conftest.py +0 -0
  57. {banks-2.1.3 → banks-2.2.0}/tests/e2e/test_completion.py +0 -0
  58. {banks-2.1.3 → banks-2.2.0}/tests/e2e/test_function_calling.py +0 -0
  59. {banks-2.1.3 → banks-2.2.0}/tests/templates/blog.jinja +0 -0
  60. {banks-2.1.3 → banks-2.2.0}/tests/templates/cache.jinja +0 -0
  61. {banks-2.1.3 → banks-2.2.0}/tests/templates/chat.jinja +0 -0
  62. {banks-2.1.3 → banks-2.2.0}/tests/templates/summarize.jinja +0 -0
  63. {banks-2.1.3 → banks-2.2.0}/tests/templates/summarize_lemma.jinja +0 -0
  64. {banks-2.1.3 → banks-2.2.0}/tests/test_audio.py +0 -0
  65. {banks-2.1.3 → banks-2.2.0}/tests/test_cache.py +0 -0
  66. {banks-2.1.3 → banks-2.2.0}/tests/test_cache_control.py +0 -0
  67. {banks-2.1.3 → banks-2.2.0}/tests/test_chat.py +0 -0
  68. {banks-2.1.3 → banks-2.2.0}/tests/test_completion.py +0 -0
  69. {banks-2.1.3 → banks-2.2.0}/tests/test_config.py +0 -0
  70. {banks-2.1.3 → banks-2.2.0}/tests/test_directory_registry.py +0 -0
  71. {banks-2.1.3 → banks-2.2.0}/tests/test_file_registry.py +0 -0
  72. {banks-2.1.3 → banks-2.2.0}/tests/test_prompt.py +0 -0
  73. {banks-2.1.3 → banks-2.2.0}/tests/test_redis_registry.py +0 -0
  74. {banks-2.1.3 → banks-2.2.0}/tests/test_tool.py +0 -0
  75. {banks-2.1.3 → banks-2.2.0}/tests/test_types.py +0 -0
  76. {banks-2.1.3 → banks-2.2.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: banks
3
- Version: 2.1.3
3
+ Version: 2.2.0
4
4
  Summary: A prompt programming language
5
5
  Project-URL: Documentation, https://github.com/masci/banks#readme
6
6
  Project-URL: Issues, https://github.com/masci/banks/issues
@@ -3,6 +3,7 @@
3
3
  - [Create a blog writing prompt](#create-a-blog-writing-prompt)
4
4
  - [Create a summarizer prompt](#create-a-summarizer-prompt)
5
5
  - [Lemmatize text while processing a template](#lemmatize-text-while-processing-a-template)
6
+ - [Convert a JSON-like object into XML while processing the template](#convert-a-json-like-object-into-xml-while-processing-the-template)
6
7
  - [Use a LLM to generate a text while rendering a prompt](#use-a-llm-to-generate-a-text-while-rendering-a-prompt)
7
8
  - [Render a prompt template as chat messages](#render-a-prompt-template-as-chat-messages)
8
9
  - [Use prompt caching from Anthropic](#use-prompt-caching-from-anthropic)
@@ -135,6 +136,57 @@ the cat be run
135
136
  Summary:
136
137
  ```
137
138
 
139
+ ## Convert a JSON-like object into XML while processing the template
140
+
141
+ Banks has built-in support for filtering JSON-like objects (Pydantic `BaseModel` subclasses, dictionaries, deserializable strings) and returning an XML string.
142
+
143
+ Here is an example of how you can use it:
144
+
145
+ ```python
146
+ from banks import Prompt
147
+ from pydantic import BaseModel
148
+ from typing import Dict
149
+
150
+ prompt_template = """
151
+ Please extract the contact details from this user:
152
+
153
+ {{ data | to_xml }}
154
+
155
+ Contact details:
156
+ """
157
+
158
+ class User(BaseModel):
159
+ username: str
160
+ account_id: str
161
+ registered_at: str
162
+ email: str
163
+ phone_number: str
164
+ social_media_accounts: Dict[str, str]
165
+
166
+ user = User(username="example", account_id="0000", registered_at="10-25-2024", email="example@email.com", phone_number="0123456789", social_media_accounts={"BlueSky": "@example.com"})
167
+
168
+ p = Prompt(prompt_template)
169
+ print(p.text({"data": user}))
170
+ ```
171
+
172
+ This will output:
173
+
174
+ ```text
175
+ Please extract the contact details from this user:
176
+
177
+ <user>
178
+ <username>example</username>
179
+ <account_id>0000</account_id>
180
+ <registered_at>10-25-2024</registered_at>
181
+ <email>example@email.com</email>
182
+ <phone_number>0123456789</phone_number>
183
+ <social_media_accounts>{'BlueSky': '@example.com'}</social_media_accounts>
184
+ </user>
185
+
186
+
187
+ Contact details:
188
+ ```
189
+
138
190
  ## Use a LLM to generate a text while rendering a prompt
139
191
 
140
192
  Sometimes it might be useful to ask another LLM to generate examples for you in a
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2023-present Massimiliano Pippi <mpippi@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "2.1.3"
4
+ __version__ = "2.2.0"
@@ -4,7 +4,7 @@
4
4
  from jinja2 import Environment, select_autoescape
5
5
 
6
6
  from .config import config
7
- from .filters import audio, cache_control, image, lemmatize, tool
7
+ from .filters import audio, cache_control, image, lemmatize, tool, xml
8
8
 
9
9
 
10
10
  def _add_extensions(_env):
@@ -38,5 +38,6 @@ env.filters["image"] = image
38
38
  env.filters["lemmatize"] = lemmatize
39
39
  env.filters["tool"] = tool
40
40
  env.filters["audio"] = audio
41
+ env.filters["to_xml"] = xml
41
42
 
42
43
  _add_extensions(env)
@@ -6,5 +6,6 @@ from .cache_control import cache_control
6
6
  from .image import image
7
7
  from .lemmatize import lemmatize
8
8
  from .tool import tool
9
+ from .xml import xml
9
10
 
10
- __all__ = ("cache_control", "image", "lemmatize", "tool", "audio")
11
+ __all__ = ("cache_control", "image", "lemmatize", "tool", "audio", "xml")
@@ -1,15 +1,25 @@
1
1
  # SPDX-FileCopyrightText: 2023-present Massimiliano Pippi <mpippi@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
+ import re
4
5
  from pathlib import Path
5
6
  from urllib.parse import urlparse
6
7
 
7
8
  from banks.types import ContentBlock, ImageUrl
8
9
 
10
+ BASE64_PATH_REGEX = re.compile(r"image\/.*;base64,.*")
11
+
9
12
 
10
13
  def _is_url(string: str) -> bool:
11
14
  result = urlparse(string)
12
- return all([result.scheme, result.netloc])
15
+ if not result.scheme:
16
+ return False
17
+
18
+ if not result.netloc:
19
+ # The only valid format when netloc is empty is base64 data urls
20
+ return all([result.scheme == "data", BASE64_PATH_REGEX.match(result.path)])
21
+
22
+ return True
13
23
 
14
24
 
15
25
  def image(value: str) -> str:
@@ -0,0 +1,62 @@
1
+ import json
2
+ import xml.etree.ElementTree as ET
3
+ from typing import Any, Optional, Union
4
+ from xml.dom.minidom import parseString
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ def _deserialize(string: str) -> Optional[dict]:
10
+ try:
11
+ return json.loads(string)
12
+ except json.JSONDecodeError:
13
+ return None
14
+
15
+
16
+ def _prepare_dictionary(value: Union[str, BaseModel, dict[str, Any]]):
17
+ root_tag = "input"
18
+ if isinstance(value, str):
19
+ model: Optional[dict[str, Any]] = _deserialize(value)
20
+ if model is None:
21
+ msg = f"{value} is not deserializable"
22
+ raise ValueError(msg)
23
+ elif isinstance(value, BaseModel):
24
+ model = value.model_dump()
25
+ root_tag = value.__class__.__name__.lower()
26
+ elif isinstance(value, dict):
27
+ model = value.copy()
28
+ for k in value.keys():
29
+ if not isinstance(k, str):
30
+ key = str(k)
31
+ if isinstance(k, (int, float)):
32
+ key = "_" + key
33
+ v = model.pop(k)
34
+ model[key.lower()] = v
35
+ else:
36
+ msg = f"Input can only be of type BaseModel, dictionary or deserializable string. Got {type(value)}"
37
+ raise ValueError(msg)
38
+ return model, root_tag
39
+
40
+
41
+ def xml(value: Union[str, BaseModel, dict[str, Any]]) -> str:
42
+ """
43
+ Convert a Pydantic model, a deserializable string or a dictionary into an XML string.
44
+
45
+ Example:
46
+ ```jinja
47
+ {{'{"username": "user", "email": "example@email.com"}' | to_xml}}
48
+ "
49
+ <input>
50
+ <username>user</username>
51
+ <email>example@email.com</email>
52
+ </input>
53
+ "
54
+ ```
55
+ """
56
+ model, root_tag = _prepare_dictionary(value)
57
+ xml_model = ET.Element(root_tag)
58
+ for k, v in model.items():
59
+ sub = ET.SubElement(xml_model, k)
60
+ sub.text = str(v)
61
+ xml_str = ET.tostring(xml_model, encoding="unicode")
62
+ return parseString(xml_str).toprettyxml().replace('<?xml version="1.0" ?>\n', "") # noqa: S318
@@ -57,6 +57,23 @@ def test_image_with_file_path(tmp_path):
57
57
  assert content_block["image_url"]["url"].startswith("data:image/jpeg;base64,")
58
58
 
59
59
 
60
+ def test_image_base64(tmp_path):
61
+ """Test image filter with a binary input"""
62
+ test_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABMgA"
63
+ result = image(test_image)
64
+
65
+ # Verify the content block wrapper
66
+ assert result.startswith("<content_block>")
67
+ assert result.endswith("</content_block>")
68
+
69
+ # Parse the JSON content
70
+ json_content = result[15:-16] # Remove wrapper tags
71
+ content_block = json.loads(json_content)
72
+
73
+ assert content_block["type"] == "image_url"
74
+ assert content_block["image_url"]["url"].startswith("data:image/png;base64,")
75
+
76
+
60
77
  def test_image_with_nonexistent_file():
61
78
  """Test image filter with a nonexistent file path"""
62
79
  with pytest.raises(FileNotFoundError):
@@ -0,0 +1,70 @@
1
+ import xml.etree.ElementTree as ET
2
+ from typing import Any
3
+ from xml.dom.minidom import parseString
4
+
5
+ import pytest
6
+ from pydantic import BaseModel
7
+
8
+ from banks.filters.xml import xml
9
+
10
+
11
+ @pytest.fixture
12
+ def xml_string_from_basemodel() -> str:
13
+ model = ET.Element("person")
14
+ age = ET.SubElement(model, "age")
15
+ age.text = "30"
16
+ name = ET.SubElement(model, "name")
17
+ name.text = "John Doe"
18
+ xml_str = ET.tostring(model, encoding="unicode")
19
+ return parseString(xml_str).toprettyxml().replace('<?xml version="1.0" ?>\n', "") # noqa: S318
20
+
21
+
22
+ @pytest.fixture
23
+ def xml_string_from_other() -> str:
24
+ model = ET.Element("input")
25
+ age = ET.SubElement(model, "age")
26
+ age.text = "30"
27
+ name = ET.SubElement(model, "name")
28
+ name.text = "John Doe"
29
+ xml_str = ET.tostring(model, encoding="unicode")
30
+ return parseString(xml_str).toprettyxml().replace('<?xml version="1.0" ?>\n', "") # noqa: S318
31
+
32
+
33
+ @pytest.fixture
34
+ def xml_string_from_edge_case() -> str:
35
+ model = ET.Element("input")
36
+ hello = ET.SubElement(model, "_1")
37
+ hello.text = "hello"
38
+ world = ET.SubElement(model, "_2.4")
39
+ world.text = "world"
40
+ xml_str = ET.tostring(model, encoding="unicode")
41
+ return parseString(xml_str).toprettyxml().replace('<?xml version="1.0" ?>\n', "") # noqa: S318
42
+
43
+
44
+ @pytest.fixture
45
+ def starting_value() -> tuple[BaseModel, dict[str, Any], str]:
46
+ class Person(BaseModel):
47
+ age: int
48
+ name: str
49
+
50
+ p = Person(age=30, name="John Doe")
51
+ return p, p.model_dump(), p.model_dump_json()
52
+
53
+
54
+ @pytest.fixture
55
+ def dict_edge_case() -> tuple[dict, str]:
56
+ return {1: "hello", 2.4: "world"}
57
+
58
+
59
+ def test_xml_filter(
60
+ xml_string_from_basemodel: str,
61
+ xml_string_from_other: str,
62
+ starting_value: tuple[BaseModel, dict[str, Any], str],
63
+ dict_edge_case: dict,
64
+ xml_string_from_edge_case: str,
65
+ ) -> None:
66
+ model, dictionary, string = starting_value
67
+ assert xml(model) == xml_string_from_basemodel
68
+ assert xml(dictionary) == xml_string_from_other
69
+ assert xml(string) == xml_string_from_other
70
+ assert xml(dict_edge_case) == xml_string_from_edge_case
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes