banks 2.2.0__tar.gz → 2.4.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 (88) hide show
  1. banks-2.4.0/.github/workflows/release.yml +79 -0
  2. {banks-2.2.0 → banks-2.4.0}/.github/workflows/test.yml +3 -3
  3. banks-2.4.0/AGENTS.md +218 -0
  4. banks-2.4.0/CLAUDE.md +3 -0
  5. {banks-2.2.0 → banks-2.4.0}/PKG-INFO +5 -3
  6. {banks-2.2.0 → banks-2.4.0}/README.md +2 -2
  7. {banks-2.2.0 → banks-2.4.0}/pyproject.toml +6 -2
  8. {banks-2.2.0 → banks-2.4.0}/src/banks/__about__.py +1 -1
  9. {banks-2.2.0 → banks-2.4.0}/src/banks/config.py +4 -1
  10. {banks-2.2.0 → banks-2.4.0}/src/banks/env.py +3 -1
  11. {banks-2.2.0 → banks-2.4.0}/src/banks/errors.py +1 -1
  12. {banks-2.2.0 → banks-2.4.0}/src/banks/extensions/completion.py +16 -2
  13. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/__init__.py +3 -1
  14. banks-2.4.0/src/banks/filters/audio.py +76 -0
  15. banks-2.4.0/src/banks/filters/document.py +135 -0
  16. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/image.py +4 -2
  17. banks-2.4.0/src/banks/filters/video.py +108 -0
  18. {banks-2.2.0 → banks-2.4.0}/src/banks/prompt.py +11 -2
  19. banks-2.4.0/src/banks/types.py +353 -0
  20. banks-2.4.0/tests/data/1x1.pdf +0 -0
  21. banks-2.4.0/tests/data/empty.mov +0 -0
  22. banks-2.4.0/tests/test_audio.py +121 -0
  23. {banks-2.2.0 → banks-2.4.0}/tests/test_cache_control.py +3 -2
  24. {banks-2.2.0 → banks-2.4.0}/tests/test_directory_registry.py +1 -1
  25. banks-2.4.0/tests/test_document.py +167 -0
  26. {banks-2.2.0 → banks-2.4.0}/tests/test_image.py +35 -0
  27. {banks-2.2.0 → banks-2.4.0}/tests/test_redis_registry.py +1 -1
  28. banks-2.4.0/tests/test_video.py +126 -0
  29. banks-2.2.0/.github/workflows/release.yml +0 -33
  30. banks-2.2.0/CLAUDE.md +0 -126
  31. banks-2.2.0/src/banks/filters/audio.py +0 -23
  32. banks-2.2.0/src/banks/types.py +0 -184
  33. banks-2.2.0/tests/test_audio.py +0 -45
  34. {banks-2.2.0 → banks-2.4.0}/.github/workflows/docs.yml +0 -0
  35. {banks-2.2.0 → banks-2.4.0}/.gitignore +0 -0
  36. {banks-2.2.0 → banks-2.4.0}/CITATION.cff +0 -0
  37. {banks-2.2.0 → banks-2.4.0}/CODE_OF_CONDUCT.md +0 -0
  38. {banks-2.2.0 → banks-2.4.0}/CONTRIBUTING.md +0 -0
  39. {banks-2.2.0 → banks-2.4.0}/LICENSE.txt +0 -0
  40. {banks-2.2.0 → banks-2.4.0}/MANIFEST.in +0 -0
  41. {banks-2.2.0 → banks-2.4.0}/assets/banks.png +0 -0
  42. {banks-2.2.0 → banks-2.4.0}/cookbook/Prompt_Caching_with_Anthropic.ipynb +0 -0
  43. {banks-2.2.0 → banks-2.4.0}/cookbook/Prompt_Versioning.ipynb +0 -0
  44. {banks-2.2.0 → banks-2.4.0}/cookbook/in_prompt_completion.ipynb +0 -0
  45. {banks-2.2.0 → banks-2.4.0}/docs/config.md +0 -0
  46. {banks-2.2.0 → banks-2.4.0}/docs/examples.md +0 -0
  47. {banks-2.2.0 → banks-2.4.0}/docs/index.md +0 -0
  48. {banks-2.2.0 → banks-2.4.0}/docs/prompt.md +0 -0
  49. {banks-2.2.0 → banks-2.4.0}/docs/python.md +0 -0
  50. {banks-2.2.0 → banks-2.4.0}/docs/registry.md +0 -0
  51. {banks-2.2.0 → banks-2.4.0}/mkdocs.yml +0 -0
  52. {banks-2.2.0 → banks-2.4.0}/src/banks/__init__.py +0 -0
  53. {banks-2.2.0 → banks-2.4.0}/src/banks/cache.py +0 -0
  54. {banks-2.2.0 → banks-2.4.0}/src/banks/extensions/__init__.py +0 -0
  55. {banks-2.2.0 → banks-2.4.0}/src/banks/extensions/chat.py +0 -0
  56. {banks-2.2.0 → banks-2.4.0}/src/banks/extensions/docs.py +0 -0
  57. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/cache_control.py +0 -0
  58. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/lemmatize.py +0 -0
  59. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/tool.py +0 -0
  60. {banks-2.2.0 → banks-2.4.0}/src/banks/filters/xml.py +0 -0
  61. {banks-2.2.0 → banks-2.4.0}/src/banks/registries/__init__.py +0 -0
  62. {banks-2.2.0 → banks-2.4.0}/src/banks/registries/directory.py +0 -0
  63. {banks-2.2.0 → banks-2.4.0}/src/banks/registries/file.py +0 -0
  64. {banks-2.2.0 → banks-2.4.0}/src/banks/registries/redis.py +0 -0
  65. {banks-2.2.0 → banks-2.4.0}/src/banks/utils.py +0 -0
  66. {banks-2.2.0 → banks-2.4.0}/tests/__init__.py +0 -0
  67. {banks-2.2.0 → banks-2.4.0}/tests/conftest.py +0 -0
  68. {banks-2.2.0 → banks-2.4.0}/tests/data/1x1.png +0 -0
  69. {banks-2.2.0 → banks-2.4.0}/tests/data/empty.wav +0 -0
  70. {banks-2.2.0 → banks-2.4.0}/tests/e2e/__init__.py +0 -0
  71. {banks-2.2.0 → banks-2.4.0}/tests/e2e/conftest.py +0 -0
  72. {banks-2.2.0 → banks-2.4.0}/tests/e2e/test_completion.py +0 -0
  73. {banks-2.2.0 → banks-2.4.0}/tests/e2e/test_function_calling.py +0 -0
  74. {banks-2.2.0 → banks-2.4.0}/tests/templates/blog.jinja +0 -0
  75. {banks-2.2.0 → banks-2.4.0}/tests/templates/cache.jinja +0 -0
  76. {banks-2.2.0 → banks-2.4.0}/tests/templates/chat.jinja +0 -0
  77. {banks-2.2.0 → banks-2.4.0}/tests/templates/summarize.jinja +0 -0
  78. {banks-2.2.0 → banks-2.4.0}/tests/templates/summarize_lemma.jinja +0 -0
  79. {banks-2.2.0 → banks-2.4.0}/tests/test_cache.py +0 -0
  80. {banks-2.2.0 → banks-2.4.0}/tests/test_chat.py +0 -0
  81. {banks-2.2.0 → banks-2.4.0}/tests/test_completion.py +0 -0
  82. {banks-2.2.0 → banks-2.4.0}/tests/test_config.py +0 -0
  83. {banks-2.2.0 → banks-2.4.0}/tests/test_file_registry.py +0 -0
  84. {banks-2.2.0 → banks-2.4.0}/tests/test_prompt.py +0 -0
  85. {banks-2.2.0 → banks-2.4.0}/tests/test_tool.py +0 -0
  86. {banks-2.2.0 → banks-2.4.0}/tests/test_types.py +0 -0
  87. {banks-2.2.0 → banks-2.4.0}/tests/test_utils.py +0 -0
  88. {banks-2.2.0 → banks-2.4.0}/tests/test_xml.py +0 -0
@@ -0,0 +1,79 @@
1
+ name: PyPI Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ bump:
7
+ description: "Version bump type"
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - MINOR
12
+ - BUGFIX
13
+ default: BUGFIX
14
+
15
+ jobs:
16
+ release:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: write
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+ with:
25
+ ref: ${{ github.event.repository.default_branch }}
26
+
27
+ - name: Bump version
28
+ run: |
29
+ CURRENT=$(sed -n 's/^__version__ = "\([0-9.]*\).*/\1/p' src/banks/__about__.py)
30
+ IFS=. read -r major minor patch <<< "$CURRENT"
31
+ case "${{ github.event.inputs.bump }}" in
32
+ BUGFIX)
33
+ patch=$((patch + 1))
34
+ ;;
35
+ MINOR)
36
+ minor=$((minor + 1))
37
+ patch=0
38
+ ;;
39
+ *)
40
+ echo "Unexpected bump type: ${{ github.event.inputs.bump }}"
41
+ exit 1
42
+ ;;
43
+ esac
44
+ VERSION="${major}.${minor}.${patch}"
45
+ echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
46
+ echo "Bumped ${CURRENT} -> ${VERSION} (${{ github.event.inputs.bump }})"
47
+
48
+ - name: Update __about__.py on default branch
49
+ run: |
50
+ git config user.name "github-actions[bot]"
51
+ git config user.email "github-actions[bot]@users.noreply.github.com"
52
+ sed -i "s/^__version__ = .*$/__version__ = \"${VERSION}\"/" src/banks/__about__.py
53
+ git diff --quiet && exit 0
54
+ git add src/banks/__about__.py
55
+ git commit -m "chore: set __version__ to ${VERSION} [skip ci]"
56
+ git push origin "${{ github.event.repository.default_branch }}"
57
+
58
+ - name: Create and push tag
59
+ run: |
60
+ git tag "v${VERSION}"
61
+ git push origin "v${VERSION}"
62
+
63
+ - name: Install Hatch
64
+ run: pip install hatch
65
+
66
+ - name: Publish on PyPi
67
+ env:
68
+ HATCH_INDEX_USER: __token__
69
+ HATCH_INDEX_AUTH: ${{ secrets.PYPI_API_TOKEN }}
70
+ run: |
71
+ hatch build
72
+ hatch publish -y
73
+
74
+ - name: Create GitHub Release
75
+ uses: ncipollo/release-action@v1
76
+ with:
77
+ tag: v${{ env.VERSION }}
78
+ artifacts: "dist/*"
79
+ generateReleaseNotes: true
@@ -34,7 +34,7 @@ jobs:
34
34
  strategy:
35
35
  fail-fast: false
36
36
  matrix:
37
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
37
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
38
38
 
39
39
  steps:
40
40
  - uses: actions/checkout@v4
@@ -72,7 +72,7 @@ jobs:
72
72
  strategy:
73
73
  fail-fast: false
74
74
  matrix:
75
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
75
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
76
76
 
77
77
  steps:
78
78
  - uses: actions/checkout@v4
@@ -95,7 +95,7 @@ jobs:
95
95
  strategy:
96
96
  fail-fast: false
97
97
  matrix:
98
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
98
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
99
99
 
100
100
  steps:
101
101
  - uses: actions/checkout@v4
banks-2.4.0/AGENTS.md ADDED
@@ -0,0 +1,218 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI coding assistants when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Banks is a Python prompt programming language and templating system for LLM applications. It provides a Jinja2-based template engine with specialized extensions and filters for creating dynamic prompts, managing chat messages, handling multimodal content (images/audio/video/documents), and integrating with various LLM providers through LiteLLM.
8
+
9
+ ## Quick Reference
10
+
11
+ ```bash
12
+ # Most common commands
13
+ hatch run test # Run unit tests
14
+ hatch run lint:all # Run all linting checks
15
+ hatch run lint:fmt # Auto-format code
16
+ hatch run test tests/test_foo.py # Run specific test file
17
+ ```
18
+
19
+ ## Development Commands
20
+
21
+ ### Testing
22
+ - Run tests: `hatch run test`
23
+ - Run tests with coverage: `hatch run test-cov`
24
+ - Generate coverage report: `hatch run cov`
25
+ - Run specific test file: `hatch run test tests/test_foo.py`
26
+ - Run e2e tests: `hatch run test tests/e2e/` (requires API keys)
27
+
28
+ ### Linting and Type Checking
29
+ - Format code: `hatch run lint:fmt`
30
+ - Auto-fix lint issues: `hatch run lint:fix`
31
+ - Check formatting: `hatch run lint:check`
32
+ - Run type checking: `hatch run lint:typing`
33
+ - Run pylint: `hatch run lint:lint`
34
+ - Run all lint checks: `hatch run lint:all`
35
+
36
+ ### Documentation
37
+ - Build docs: `hatch run docs build`
38
+ - Serve docs locally: `hatch run docs serve` (available at http://127.0.0.1:8000/)
39
+
40
+ ### Environment Management
41
+ - All commands use Hatch environments with automatic dependency management
42
+ - Uses `uv` as the installer for faster dependency resolution
43
+ - Python 3.9+ supported (tested on 3.10-3.14)
44
+
45
+ ## Architecture Overview
46
+
47
+ ### Core Components
48
+
49
+ **Prompt Classes** (`src/banks/prompt.py`):
50
+ - `BasePrompt`: Base class with template rendering, metadata, versioning, and caching
51
+ - `Prompt`: Synchronous prompt rendering with `text()` and `chat_messages()` methods
52
+ - `AsyncPrompt`: Asynchronous version (requires `BANKS_ASYNC_ENABLED=true`)
53
+ - `PromptRegistry`: Protocol interface for prompt storage backends
54
+
55
+ **Type System** (`src/banks/types.py`):
56
+ - `ChatMessage`: Core chat message structure with role and content
57
+ - `ContentBlock`: Handles different content types (text, image_url, audio, video, document) with optional cache control
58
+ - `Tool`: Function calling support with automatic schema generation from Python callables
59
+ - `CacheControl`: Anthropic-style prompt caching metadata
60
+
61
+ **Template Environment** (`src/banks/env.py`):
62
+ - Global Jinja2 environment with Banks-specific extensions and filters
63
+ - Async support detection and configuration
64
+ - Custom template loader integration
65
+
66
+ **Error Types** (`src/banks/errors.py`):
67
+ - `MissingDependencyError`: Optional dependencies not installed
68
+ - `AsyncError`: Asyncio support misconfiguration
69
+ - `CanaryWordError`: Canary word leaked (prompt injection detection)
70
+ - `PromptNotFoundError`: Prompt not found in registry
71
+ - `InvalidPromptError`: Invalid prompt format
72
+ - `LLMError`: LLM provider errors
73
+
74
+ ### Extensions System
75
+
76
+ **Chat Extension** (`src/banks/extensions/chat.py`):
77
+ - `{% chat role="..." %}...{% endchat %}` blocks for structured message creation
78
+ - Automatic conversion to `ChatMessage` objects during rendering
79
+
80
+ **Completion Extension** (`src/banks/extensions/completion.py`):
81
+ - `{% completion model="..." %}...{% endcompletion %}` for in-prompt LLM calls
82
+ - Integrated with LiteLLM for multi-provider support
83
+ - Function calling support within completion blocks
84
+
85
+ ### Filters System
86
+
87
+ **Core Filters** (`src/banks/filters/`):
88
+ - `image`: Convert file paths/URLs/bytes to base64-encoded image content blocks
89
+ - `audio`: Convert audio files to base64-encoded audio content blocks
90
+ - `video`: Convert video files to base64-encoded video content blocks
91
+ - `document`: Convert documents (PDF, TXT, HTML, CSS, XML, CSV, RTF, JS, JSON) to base64-encoded content blocks
92
+ - `cache_control`: Add Anthropic cache control metadata to content blocks
93
+ - `tool`: Convert Python callables to LLM function call schemas
94
+ - `lemmatize`: Text lemmatization using simplemma
95
+
96
+ **Filter Pattern**: Filters wrap content in `<content_block>` tags and are only useful within `{% chat %}` blocks.
97
+
98
+ ### Registry System
99
+
100
+ **Storage Backends** (`src/banks/registries/`):
101
+ - `DirectoryTemplateRegistry`: File system-based prompt storage
102
+ - `FileTemplateRegistry`: Single file-based storage
103
+ - `RedisTemplateRegistry`: Redis-backed storage for distributed scenarios
104
+ - All registries implement the `PromptRegistry` protocol
105
+
106
+ ### Caching System
107
+
108
+ **Render Cache** (`src/banks/cache.py`):
109
+ - `RenderCache`: Protocol interface for caching rendered prompts
110
+ - `DefaultCache`: In-memory cache using pickle-serialized context as key
111
+ - Prevents re-rendering identical template + context combinations
112
+
113
+ ### Configuration
114
+
115
+ **Config System** (`src/banks/config.py`):
116
+ - Environment variable-based configuration with `BANKS_` prefix
117
+ - `BANKS_ASYNC_ENABLED`: Enable async template rendering (must be set before import)
118
+ - `BANKS_USER_DATA_PATH`: Custom user data directory
119
+
120
+ ## Key Development Patterns
121
+
122
+ ### Template Rendering Flow
123
+ 1. Templates parsed by Jinja2 environment with Banks extensions
124
+ 2. Chat blocks converted to JSON during rendering
125
+ 3. `chat_messages()` parses JSON back to `ChatMessage` objects
126
+ 4. Caching layer prevents re-rendering identical contexts
127
+
128
+ ### Multimodal Content Handling
129
+ - Images/audio/video/documents converted to base64 during filter application
130
+ - Filters accept file paths, URLs, or raw bytes
131
+ - Content blocks maintain type safety and metadata
132
+ - Cache control integrated at content block level
133
+
134
+ ### Function Calling Integration
135
+ - Python functions automatically converted to LLM schemas via introspection
136
+ - Docstring parsing for parameter descriptions
137
+ - Type annotations converted to JSON Schema
138
+
139
+ ### Async Support Architecture
140
+ - Global environment state requires async decision at import time
141
+ - `BANKS_ASYNC_ENABLED` must be set before importing banks modules
142
+ - `AsyncPrompt` provides `await`-able rendering methods
143
+
144
+ ## Testing
145
+
146
+ ### Test Markers
147
+ - `@pytest.mark.e2e`: End-to-end tests requiring external services
148
+ - `@pytest.mark.redis`: Tests requiring a running Redis instance
149
+
150
+ ### Required Environment Variables for E2E Tests
151
+ - `OPENAI_API_KEY`: For OpenAI-based tests
152
+ - `ANTHROPIC_API_KEY`: For Anthropic-based tests
153
+
154
+ ### Test Data
155
+ - Test fixtures in `tests/data/` (images, audio, video, PDFs)
156
+ - Template examples in `tests/templates/`
157
+
158
+ ### Running Specific Tests
159
+ ```bash
160
+ hatch run test tests/test_image.py # Single file
161
+ hatch run test tests/test_image.py::test_name # Single test
162
+ hatch run test -k "image" # Tests matching pattern
163
+ ```
164
+
165
+ ## Code Style
166
+
167
+ ### Formatting
168
+ - Line length: 120 characters
169
+ - Use ruff for formatting and linting
170
+ - Imports sorted with `banks` as first-party
171
+
172
+ ### Type Hints
173
+ - All public functions should have type annotations
174
+ - Use `from __future__ import annotations` for forward references
175
+ - MyPy strict mode enforced
176
+
177
+ ### Conventions
178
+ - SPDX license headers in all source files
179
+ - Docstrings for public APIs
180
+ - Relative imports banned (use absolute `from banks.x import y`)
181
+
182
+ ## Public API
183
+
184
+ The main exports from `banks` package:
185
+ ```python
186
+ from banks import Prompt, AsyncPrompt, ChatMessage, config, env
187
+ ```
188
+
189
+ ## Dependencies
190
+
191
+ **Core (required)**:
192
+ - `jinja2`: Core templating engine
193
+ - `pydantic`: Type validation and serialization
194
+ - `griffe`: Code introspection utilities
195
+ - `platformdirs`: Cross-platform data directory handling
196
+ - `filetype`: File type detection for multimodal content
197
+ - `deprecated`: Deprecation decorators
198
+
199
+ **Optional**:
200
+ - `litellm`: Multi-provider LLM integration (`banks[all]`)
201
+ - `redis`: Redis registry backend (`banks[all]`)
202
+ - `simplemma`: Lemmatization filter (dev dependency)
203
+
204
+ ## CI/CD
205
+
206
+ - **test.yml**: Runs tests on Python 3.10-3.14
207
+ - **docs.yml**: Builds and deploys documentation
208
+ - **release.yml**: Handles package releases
209
+
210
+ ## PR Guidelines
211
+
212
+ Follow conventional commit prefixes for PR titles:
213
+ - `fix:` - Bug fixes
214
+ - `feat:` - New features
215
+ - `chore:` - Maintenance
216
+ - `docs:` - Documentation
217
+ - `refactor:` - Code refactoring
218
+ - `test:` - Test additions/changes
banks-2.4.0/CLAUDE.md ADDED
@@ -0,0 +1,3 @@
1
+ # CLAUDE.md
2
+
3
+ See [AGENTS.md](./AGENTS.md) for development guidance and project context.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: banks
3
- Version: 2.2.0
3
+ Version: 2.4.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
@@ -15,11 +15,13 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
21
  Requires-Python: >=3.9
21
22
  Requires-Dist: deprecated
22
23
  Requires-Dist: eval-type-backport; python_version < '3.10'
24
+ Requires-Dist: filetype>=1.2.0
23
25
  Requires-Dist: griffe
24
26
  Requires-Dist: jinja2
25
27
  Requires-Dist: platformdirs
@@ -125,11 +127,11 @@ print(p.chat_messages({"persona": "helpful assistant"}))
125
127
  # [
126
128
  # ChatMessage(role='system', content=[
127
129
  # ContentBlock(type=<ContentBlockType.text: 'text'>, cache_control=None, text='You are a helpful assistant.',
128
- # image_url=None, input_audio=None)
130
+ # image_url=None, input_audio=None, input_video=None, input_document=None)
129
131
  # ], tool_call_id=None, name=None),
130
132
  # ChatMessage(role='user', content=[
131
133
  # ContentBlock(type=<ContentBlockType.text: 'text'>, cache_control=None, text='Hello, how are you?',
132
- # image_url=None, input_audio=None)
134
+ # image_url=None, input_audio=None, input_video=None, input_document=None)
133
135
  # ], tool_call_id=None, name=None)
134
136
  # ]
135
137
  ```
@@ -94,11 +94,11 @@ print(p.chat_messages({"persona": "helpful assistant"}))
94
94
  # [
95
95
  # ChatMessage(role='system', content=[
96
96
  # ContentBlock(type=<ContentBlockType.text: 'text'>, cache_control=None, text='You are a helpful assistant.',
97
- # image_url=None, input_audio=None)
97
+ # image_url=None, input_audio=None, input_video=None, input_document=None)
98
98
  # ], tool_call_id=None, name=None),
99
99
  # ChatMessage(role='user', content=[
100
100
  # ContentBlock(type=<ContentBlockType.text: 'text'>, cache_control=None, text='Hello, how are you?',
101
- # image_url=None, input_audio=None)
101
+ # image_url=None, input_audio=None, input_video=None, input_document=None)
102
102
  # ], tool_call_id=None, name=None)
103
103
  # ]
104
104
  ```
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
@@ -29,6 +30,7 @@ dependencies = [
29
30
  "deprecated",
30
31
  "eval-type-backport;python_version<'3.10'",
31
32
  "platformdirs",
33
+ "filetype>=1.2.0",
32
34
  ]
33
35
 
34
36
  [project.optional-dependencies]
@@ -65,7 +67,7 @@ cov = ["test-cov", "cov-report"]
65
67
  docs = "mkdocs {args:build}"
66
68
 
67
69
  [[tool.hatch.envs.all.matrix]]
68
- python = ["3.9", "3.10", "3.11", "3.12", "3.13"]
70
+ python = ["3.10", "3.11", "3.12", "3.13", "3.14"]
69
71
 
70
72
  [tool.hatch.envs.lint]
71
73
  detached = false # Normally the linting env can be detached, but mypy doesn't install all the stubs we need
@@ -77,6 +79,7 @@ lint = "pylint {args:src/banks}"
77
79
  typing = "mypy --install-types --non-interactive {args:src/banks}"
78
80
  all = ["check", "typing", "lint"]
79
81
  fmt = "ruff format {args}"
82
+ fix = "ruff check --fix {args}"
80
83
 
81
84
  [tool.hatch.build.targets.wheel]
82
85
  only-include = ["src/banks", "src/templates"]
@@ -182,6 +185,7 @@ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
182
185
  module = ["litellm.*", "simplemma.*", "deprecated.*"]
183
186
  ignore_missing_imports = true
184
187
 
188
+
185
189
  [tool.pylint]
186
190
  disable = [
187
191
  "line-too-long",
@@ -199,7 +203,7 @@ max-args = 10
199
203
  asyncio_default_fixture_loop_scope = "function"
200
204
  markers = ["e2e"]
201
205
  filterwarnings = [
202
- # Dilence litellm warning coming from their Pydantic config.
206
+ # Silence litellm warning coming from their Pydantic config.
203
207
  # This assumes our use of Pydantic is correct :)
204
208
  "ignore:Support for class-based `config` is deprecated",
205
209
  ]
@@ -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.2.0"
4
+ __version__ = "2.4.0"
@@ -28,9 +28,12 @@ class _BanksConfig:
28
28
  return original_value
29
29
 
30
30
  # Convert string from env var to the actual type
31
- t = super().__getattribute__("__annotations__")[name]
31
+ annotations = getattr(type(self), "__annotations__", {})
32
+ t = annotations.get(name, type(original_value))
32
33
  if t is bool:
33
34
  return strtobool(read_value)
35
+ if t is Any:
36
+ return read_value
34
37
 
35
38
  return t(read_value)
36
39
 
@@ -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, xml
7
+ from .filters import audio, cache_control, document, image, lemmatize, tool, video, xml
8
8
 
9
9
 
10
10
  def _add_extensions(_env):
@@ -38,6 +38,8 @@ 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["video"] = video
42
+ env.filters["document"] = document
41
43
  env.filters["to_xml"] = xml
42
44
 
43
45
  _add_extensions(env)
@@ -14,7 +14,7 @@ class CanaryWordError(Exception):
14
14
 
15
15
 
16
16
  class PromptNotFoundError(Exception):
17
- """The prompt was now found in the registry."""
17
+ """The prompt was not found in the registry."""
18
18
 
19
19
 
20
20
  class InvalidPromptError(Exception):
@@ -3,7 +3,7 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
  import importlib
5
5
  import json
6
- from typing import cast
6
+ from typing import TYPE_CHECKING, Any, Callable, cast
7
7
 
8
8
  from jinja2 import TemplateSyntaxError, nodes
9
9
  from jinja2.ext import Extension
@@ -12,6 +12,8 @@ from pydantic import ValidationError
12
12
  from banks.errors import InvalidPromptError, LLMError
13
13
  from banks.types import ChatMessage, Tool
14
14
 
15
+ if TYPE_CHECKING:
16
+ from litellm.types.utils import ChatCompletionMessageToolCall
15
17
  SUPPORTED_KWARGS = ("model",)
16
18
  LITELLM_INSTALL_MSG = "litellm is not installed. Please install it with `pip install litellm`."
17
19
 
@@ -74,7 +76,19 @@ class CompletionExtension(Extension):
74
76
  return nodes.CallBlock(self.call_method("_do_completion_async", args), [], [], body).set_lineno(lineno)
75
77
  return nodes.CallBlock(self.call_method("_do_completion", args), [], [], body).set_lineno(lineno)
76
78
 
77
- def _get_tool_callable(self, tools, tool_call):
79
+ def _get_tool_callable(self, tools: list[Tool], tool_call: "ChatCompletionMessageToolCall") -> Callable[..., Any]:
80
+ """Get the callable function for a tool call.
81
+
82
+ Args:
83
+ tools: List of available tools
84
+ tool_call: The tool call from the LLM response
85
+
86
+ Returns:
87
+ The callable function
88
+
89
+ Raises:
90
+ ValueError: If the function is not found in available tools
91
+ """
78
92
  for tool in tools:
79
93
  if tool.function.name == tool_call.function.name:
80
94
  module_name, func_name = tool.import_path.rsplit(".", maxsplit=1)
@@ -3,9 +3,11 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
  from .audio import audio
5
5
  from .cache_control import cache_control
6
+ from .document import document
6
7
  from .image import image
7
8
  from .lemmatize import lemmatize
8
9
  from .tool import tool
10
+ from .video import video
9
11
  from .xml import xml
10
12
 
11
- __all__ = ("cache_control", "image", "lemmatize", "tool", "audio", "xml")
13
+ __all__ = ("cache_control", "image", "lemmatize", "tool", "audio", "video", "document", "xml")
@@ -0,0 +1,76 @@
1
+ # SPDX-FileCopyrightText: 2023-present Massimiliano Pippi <mpippi@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ import re
5
+ from pathlib import Path
6
+ from typing import cast
7
+ from urllib.parse import urlparse
8
+
9
+ import filetype # type: ignore[import-untyped]
10
+
11
+ from banks.types import AudioFormat, ContentBlock, InputAudio, resolve_binary
12
+
13
+ BASE64_AUDIO_REGEX = re.compile(r"audio\/.*;base64,.*")
14
+
15
+
16
+ def _is_url(string: str) -> bool:
17
+ """Check if a string is a URL."""
18
+ result = urlparse(string)
19
+ if not result.scheme:
20
+ return False
21
+
22
+ if not result.netloc:
23
+ # The only valid format when netloc is empty is base64 data urls
24
+ return all([result.scheme == "data", BASE64_AUDIO_REGEX.match(result.path)])
25
+
26
+ return True
27
+
28
+
29
+ def _get_audio_format_from_url(url: str) -> AudioFormat:
30
+ """Extract audio format from URL.
31
+
32
+ Tries to determine format from URL path or defaults to mp3.
33
+ """
34
+ parsed = urlparse(url)
35
+ path = parsed.path.lower()
36
+ for fmt in ("mp3", "wav", "m4a", "webm", "ogg", "flac"):
37
+ if path.endswith(f".{fmt}"):
38
+ return cast(AudioFormat, fmt)
39
+ # Default to mp3 if format cannot be determined
40
+ return "mp3"
41
+
42
+
43
+ def _get_audio_format_from_bytes(data: bytes) -> AudioFormat:
44
+ """Extract audio format from bytes data using filetype library."""
45
+ kind = filetype.guess(data)
46
+ if kind is not None:
47
+ fmt = kind.extension
48
+ if fmt in ("mp3", "wav", "m4a", "webm", "ogg", "flac"):
49
+ return cast(AudioFormat, fmt)
50
+ # Default to mp3 if format cannot be determined
51
+ return "mp3"
52
+
53
+
54
+ def audio(value: str | bytes) -> str:
55
+ """Wrap the filtered value into a ContentBlock of type audio.
56
+
57
+ The resulting ChatMessage will have the field `content` populated with a list of ContentBlock objects.
58
+
59
+ Supports both file paths and URLs (including data URLs).
60
+
61
+ Example:
62
+ ```jinja
63
+ {{ "path/to/audio/file.mp3" | audio }}
64
+ {{ "https://example.com/audio.mp3" | audio }}
65
+ ```
66
+ """
67
+ if isinstance(value, bytes):
68
+ audio_format = _get_audio_format_from_bytes(resolve_binary(value, as_base64=False))
69
+ input_audio = InputAudio.from_bytes(value, audio_format=audio_format)
70
+ elif _is_url(value):
71
+ audio_format = _get_audio_format_from_url(value)
72
+ input_audio = InputAudio.from_url(value, audio_format)
73
+ else:
74
+ input_audio = InputAudio.from_path(Path(value))
75
+ block = ContentBlock.model_validate({"type": "audio", "input_audio": input_audio})
76
+ return f"<content_block>{block.model_dump_json()}</content_block>"