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.
- {banks-2.1.3 → banks-2.2.0}/PKG-INFO +1 -1
- {banks-2.1.3 → banks-2.2.0}/docs/examples.md +52 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/__about__.py +1 -1
- {banks-2.1.3 → banks-2.2.0}/src/banks/env.py +2 -1
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/__init__.py +2 -1
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/image.py +11 -1
- banks-2.2.0/src/banks/filters/xml.py +62 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_image.py +17 -0
- banks-2.2.0/tests/test_xml.py +70 -0
- {banks-2.1.3 → banks-2.2.0}/.github/workflows/docs.yml +0 -0
- {banks-2.1.3 → banks-2.2.0}/.github/workflows/release.yml +0 -0
- {banks-2.1.3 → banks-2.2.0}/.github/workflows/test.yml +0 -0
- {banks-2.1.3 → banks-2.2.0}/.gitignore +0 -0
- {banks-2.1.3 → banks-2.2.0}/CITATION.cff +0 -0
- {banks-2.1.3 → banks-2.2.0}/CLAUDE.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/CONTRIBUTING.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/LICENSE.txt +0 -0
- {banks-2.1.3 → banks-2.2.0}/MANIFEST.in +0 -0
- {banks-2.1.3 → banks-2.2.0}/README.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/assets/banks.png +0 -0
- {banks-2.1.3 → banks-2.2.0}/cookbook/Prompt_Caching_with_Anthropic.ipynb +0 -0
- {banks-2.1.3 → banks-2.2.0}/cookbook/Prompt_Versioning.ipynb +0 -0
- {banks-2.1.3 → banks-2.2.0}/cookbook/in_prompt_completion.ipynb +0 -0
- {banks-2.1.3 → banks-2.2.0}/docs/config.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/docs/index.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/docs/prompt.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/docs/python.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/docs/registry.md +0 -0
- {banks-2.1.3 → banks-2.2.0}/mkdocs.yml +0 -0
- {banks-2.1.3 → banks-2.2.0}/pyproject.toml +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/__init__.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/cache.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/config.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/errors.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/__init__.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/chat.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/completion.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/extensions/docs.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/audio.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/cache_control.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/lemmatize.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/filters/tool.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/prompt.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/registries/__init__.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/registries/directory.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/registries/file.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/registries/redis.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/types.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/src/banks/utils.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/__init__.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/conftest.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/data/1x1.png +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/data/empty.wav +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/e2e/__init__.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/e2e/conftest.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/e2e/test_completion.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/e2e/test_function_calling.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/templates/blog.jinja +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/templates/cache.jinja +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/templates/chat.jinja +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/templates/summarize.jinja +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/templates/summarize_lemma.jinja +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_audio.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_cache.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_cache_control.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_chat.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_completion.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_config.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_directory_registry.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_file_registry.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_prompt.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_redis_registry.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_tool.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_types.py +0 -0
- {banks-2.1.3 → banks-2.2.0}/tests/test_utils.py +0 -0
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
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
|