llmling-models 0.0.2__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.
@@ -0,0 +1,21 @@
1
+ # Changes here will be overwritten by Copier
2
+ _commit: v1.8.1
3
+ _src_path: .\copier-phil65\
4
+ author_email: philipptemminghoff@googlemail.com
5
+ author_fullname: Philipp Temminghoff
6
+ author_username: phil65
7
+ copyright_date: '2024'
8
+ copyright_holder: Philipp Temminghoff
9
+ copyright_holder_email: philipptemminghoff@googlemail.com
10
+ libraries_use_pydantic: true
11
+ libraries_use_qt: false
12
+ project_description: Pydantic-AI models for LLMling-agent
13
+ project_name: LLMling-models
14
+ python_minimum_version: '3.12'
15
+ python_package_command_line_name: llmling-models
16
+ python_package_distribution_name: llmling-models
17
+ python_package_import_name: llmling_models
18
+ repository_name: llmling-models
19
+ repository_namespace: phil65
20
+ repository_provider: github.com
21
+
@@ -0,0 +1,3 @@
1
+ github: phil65
2
+ custom:
3
+ - https://www.paypal.me/phil65
@@ -0,0 +1,19 @@
1
+ ## Code style
2
+
3
+ You can assume Python 3.12.
4
+ Use newest language features if possible
5
+ Consider using pattern matching, walrus operators and other new syntax features.
6
+ Adhere to pep8.
7
+
8
+ ## Tests
9
+
10
+ Tests are written using PyTest. Dont put tests into a class.
11
+
12
+ ## DocStrings
13
+
14
+ DocStrings are written in google-style.
15
+ Dont add the types to the "Args" section.
16
+ Only add a Return section if a value is returned and the meaning of the returned value
17
+ is not obvious.
18
+ You can use markdown admonitions with python-markdown syntax if neccessary (!!! info, !!! note, !!! warning).
19
+ You can use markdown tables and markdown lists if neccessary.
@@ -0,0 +1,9 @@
1
+ # Set update schedule for GitHub Actions
2
+
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: "github-actions"
6
+ directory: "/"
7
+ schedule:
8
+ # Check for updates to GitHub Actions every week
9
+ interval: "weekly"
@@ -0,0 +1,88 @@
1
+ name: Build
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ${{ matrix.os }}
8
+ continue-on-error: ${{ matrix.python_version == '3.14' }}
9
+ strategy:
10
+ matrix:
11
+ python_version: ["3.12", "3.13", "3.14"]
12
+ os: ["ubuntu-latest", "macos-latest", "windows-latest"]
13
+ steps:
14
+ - name: Check out repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python ${{ matrix.python_version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python_version }}
21
+ allow-prereleases: true
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v4
25
+ with:
26
+ enable-cache: true
27
+ cache-dependency-glob: pyproject.toml
28
+ cache-suffix: py${{ matrix.python_version }}
29
+
30
+ - name: Install dependencies (uv sync)
31
+ run: uv sync --all-extras --no-group docs
32
+
33
+ - name: Check for code issues (ruff check)
34
+ uses: astral-sh/ruff-action@v1
35
+
36
+ - name: Check code format (ruff format)
37
+ uses: astral-sh/ruff-action@v1
38
+ with:
39
+ args: "format --check"
40
+
41
+ - name: Static type checking (MyPy)
42
+ run: uv run --no-group docs mypy src/llmling_models/
43
+
44
+ - name: Run tests
45
+ run: uv run --no-group docs pytest --cov-report=xml --cov=src/llmling_models/ --cov-report=term-missing
46
+
47
+ - name: Upload test results to Codecov
48
+ uses: codecov/codecov-action@v5
49
+ with:
50
+ fail_ci_if_error: false
51
+ verbose: true
52
+
53
+ release:
54
+ runs-on: ubuntu-latest
55
+ needs: test
56
+ if: startsWith(github.ref, 'refs/tags/')
57
+ permissions:
58
+ # this permission is mandatory for trusted publishing
59
+ id-token: write
60
+ contents: write
61
+ steps:
62
+ - name: Check out repository
63
+ uses: actions/checkout@v4
64
+
65
+ - name: Set up Python
66
+ uses: actions/setup-python@v5
67
+ with:
68
+ python-version: "3.12"
69
+
70
+ - name: Install uv
71
+ uses: astral-sh/setup-uv@v4
72
+ with:
73
+ enable-cache: true
74
+ cache-dependency-glob: pyproject.toml
75
+ cache-suffix: py${{ matrix.python_version }}
76
+
77
+ - name: Build package
78
+ run: uv build
79
+
80
+ - name: Publish package distributions to PyPI
81
+ uses: pypa/gh-action-pypi-publish@release/v1
82
+
83
+ - name: Release package on GitHub
84
+ uses: ncipollo/release-action@v1
85
+ with:
86
+ body: ${{ github.event.head_commit.message }}
87
+ artifacts: dist/*.whl,dist/*.tar.gz
88
+ token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,57 @@
1
+ name: Build documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ # Allow one concurrent deployment
15
+ concurrency:
16
+ group: "pages"
17
+ cancel-in-progress: true
18
+
19
+ # Default to bash
20
+ defaults:
21
+ run:
22
+ shell: bash
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 0
32
+ - name: Set up Python
33
+ uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.12"
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v4
38
+ - name: Install dependencies
39
+ run: |
40
+ uv sync --all-extras
41
+ - name: Build
42
+ run: uv run mknodes build
43
+ - name: Upload artifact
44
+ uses: actions/upload-pages-artifact@v3
45
+ with:
46
+ path: ./site
47
+
48
+ deploy:
49
+ environment:
50
+ name: github-pages
51
+ url: ${{ steps.deployment.outputs.page_url }}
52
+ runs-on: ubuntu-latest
53
+ needs: build
54
+ steps:
55
+ - name: Deploy to GitHub Pages
56
+ id: deployment
57
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,32 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ # Distribution / packaging
5
+ *.sublime-workspace
6
+ # Unit test / coverage reports
7
+ .coverage
8
+ coverage.xml
9
+ .cache
10
+ .hatch
11
+ .pytest_cache/
12
+ junit.xml
13
+ # Jupyter Notebook
14
+ .ipynb_checkpoints
15
+ # pyenv
16
+ .python-version
17
+ # dotenv
18
+ .env
19
+ # virtualenv
20
+ .venv
21
+ # mkdocs documentation
22
+ /site
23
+ # mypy
24
+ .mypy_cache/
25
+ # .vscode
26
+ .vscode/
27
+ # OS files
28
+ .DS_Store
29
+ # uv
30
+ uv.lock
31
+ # DiskCache cache file
32
+ ./model_cache
@@ -0,0 +1,48 @@
1
+ default_language_version:
2
+ python: python3.12
3
+ default_stages: [pre-commit]
4
+ repos:
5
+ - repo: local
6
+ hooks:
7
+ - id: pytest-check
8
+ name: pytest-check
9
+ entry: uv run pytest
10
+ language: system
11
+ # stages: [push]
12
+ types: [python]
13
+ pass_filenames: false
14
+ always_run: true
15
+
16
+ - repo: https://github.com/pre-commit/pre-commit-hooks
17
+ # https://pre-commit.com/hooks.html
18
+ rev: v5.0.0
19
+ hooks:
20
+ - id: check-added-large-files
21
+ - id: check-case-conflict
22
+ - id: check-merge-conflict
23
+ - id: check-toml
24
+ - id: check-json
25
+ - id: check-xml
26
+ - id: check-yaml
27
+ args: [--allow-multiple-documents, --unsafe]
28
+ - id: debug-statements
29
+ - id: detect-private-key
30
+
31
+ - repo: https://github.com/pre-commit/mirrors-mypy
32
+ rev: v1.13.0
33
+ hooks:
34
+ - id: mypy
35
+ additional_dependencies: [orjson, pydantic]
36
+
37
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
38
+ rev: v0.8.3
39
+ hooks:
40
+ - id: ruff
41
+ - id: ruff-format
42
+
43
+ - repo: https://github.com/commitizen-tools/commitizen
44
+ rev: v4.1.0
45
+ hooks:
46
+ - id: commitizen
47
+ stages: [commit-msg]
48
+ additional_dependencies: [typing-extensions]
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Philipp Temminghoff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: llmling-models
3
+ Version: 0.0.2
4
+ Summary: Pydantic-AI models for LLMling-agent
5
+ Project-URL: Documentation, https://phil65.github.io/llmling-models/
6
+ Project-URL: Source, https://github.com/phil65/llmling-models
7
+ Project-URL: Issues, https://github.com/phil65/llmling-models/issues
8
+ Project-URL: Discussions, https://github.com/phil65/llmling-models/discussions
9
+ Project-URL: Code coverage, https://app.codecov.io/gh/phil65/llmling-models
10
+ Author-email: Philipp Temminghoff <philipptemminghoff@googlemail.com>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2024, Philipp Temminghoff
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+
33
+ License-File: LICENSE
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Programming Language :: Python :: 3.14
42
+ Classifier: Topic :: Documentation
43
+ Classifier: Topic :: Software Development
44
+ Classifier: Topic :: Utilities
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.12
47
+ Requires-Dist: pydantic
48
+ Requires-Dist: pydantic-ai>=0.0.12
49
+ Description-Content-Type: text/markdown
50
+
51
+ # LLMling-models
52
+
53
+ [![PyPI License](https://img.shields.io/pypi/l/llmling-models.svg)](https://pypi.org/project/llmling-models/)
54
+ [![Package status](https://img.shields.io/pypi/status/llmling-models.svg)](https://pypi.org/project/llmling-models/)
55
+ [![Daily downloads](https://img.shields.io/pypi/dd/llmling-models.svg)](https://pypi.org/project/llmling-models/)
56
+ [![Weekly downloads](https://img.shields.io/pypi/dw/llmling-models.svg)](https://pypi.org/project/llmling-models/)
57
+ [![Monthly downloads](https://img.shields.io/pypi/dm/llmling-models.svg)](https://pypi.org/project/llmling-models/)
58
+ [![Distribution format](https://img.shields.io/pypi/format/llmling-models.svg)](https://pypi.org/project/llmling-models/)
59
+ [![Wheel availability](https://img.shields.io/pypi/wheel/llmling-models.svg)](https://pypi.org/project/llmling-models/)
60
+ [![Python version](https://img.shields.io/pypi/pyversions/llmling-models.svg)](https://pypi.org/project/llmling-models/)
61
+ [![Implementation](https://img.shields.io/pypi/implementation/llmling-models.svg)](https://pypi.org/project/llmling-models/)
62
+ [![Releases](https://img.shields.io/github/downloads/phil65/llmling-models/total.svg)](https://github.com/phil65/llmling-models/releases)
63
+ [![Github Contributors](https://img.shields.io/github/contributors/phil65/llmling-models)](https://github.com/phil65/llmling-models/graphs/contributors)
64
+ [![Github Discussions](https://img.shields.io/github/discussions/phil65/llmling-models)](https://github.com/phil65/llmling-models/discussions)
65
+ [![Github Forks](https://img.shields.io/github/forks/phil65/llmling-models)](https://github.com/phil65/llmling-models/forks)
66
+ [![Github Issues](https://img.shields.io/github/issues/phil65/llmling-models)](https://github.com/phil65/llmling-models/issues)
67
+ [![Github Issues](https://img.shields.io/github/issues-pr/phil65/llmling-models)](https://github.com/phil65/llmling-models/pulls)
68
+ [![Github Watchers](https://img.shields.io/github/watchers/phil65/llmling-models)](https://github.com/phil65/llmling-models/watchers)
69
+ [![Github Stars](https://img.shields.io/github/stars/phil65/llmling-models)](https://github.com/phil65/llmling-models/stars)
70
+ [![Github Repository size](https://img.shields.io/github/repo-size/phil65/llmling-models)](https://github.com/phil65/llmling-models)
71
+ [![Github last commit](https://img.shields.io/github/last-commit/phil65/llmling-models)](https://github.com/phil65/llmling-models/commits)
72
+ [![Github release date](https://img.shields.io/github/release-date/phil65/llmling-models)](https://github.com/phil65/llmling-models/releases)
73
+ [![Github language count](https://img.shields.io/github/languages/count/phil65/llmling-models)](https://github.com/phil65/llmling-models)
74
+ [![Github commits this week](https://img.shields.io/github/commit-activity/w/phil65/llmling-models)](https://github.com/phil65/llmling-models)
75
+ [![Github commits this month](https://img.shields.io/github/commit-activity/m/phil65/llmling-models)](https://github.com/phil65/llmling-models)
76
+ [![Github commits this year](https://img.shields.io/github/commit-activity/y/phil65/llmling-models)](https://github.com/phil65/llmling-models)
77
+ [![Package status](https://codecov.io/gh/phil65/llmling-models/branch/main/graph/badge.svg)](https://codecov.io/gh/phil65/llmling-models/)
78
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
79
+ [![PyUp](https://pyup.io/repos/github/phil65/llmling-models/shield.svg)](https://pyup.io/repos/github/phil65/llmling-models/)
80
+
81
+ [Read the documentation!](https://phil65.github.io/llmling-models/)
82
+
@@ -0,0 +1,32 @@
1
+ # LLMling-models
2
+
3
+ [![PyPI License](https://img.shields.io/pypi/l/llmling-models.svg)](https://pypi.org/project/llmling-models/)
4
+ [![Package status](https://img.shields.io/pypi/status/llmling-models.svg)](https://pypi.org/project/llmling-models/)
5
+ [![Daily downloads](https://img.shields.io/pypi/dd/llmling-models.svg)](https://pypi.org/project/llmling-models/)
6
+ [![Weekly downloads](https://img.shields.io/pypi/dw/llmling-models.svg)](https://pypi.org/project/llmling-models/)
7
+ [![Monthly downloads](https://img.shields.io/pypi/dm/llmling-models.svg)](https://pypi.org/project/llmling-models/)
8
+ [![Distribution format](https://img.shields.io/pypi/format/llmling-models.svg)](https://pypi.org/project/llmling-models/)
9
+ [![Wheel availability](https://img.shields.io/pypi/wheel/llmling-models.svg)](https://pypi.org/project/llmling-models/)
10
+ [![Python version](https://img.shields.io/pypi/pyversions/llmling-models.svg)](https://pypi.org/project/llmling-models/)
11
+ [![Implementation](https://img.shields.io/pypi/implementation/llmling-models.svg)](https://pypi.org/project/llmling-models/)
12
+ [![Releases](https://img.shields.io/github/downloads/phil65/llmling-models/total.svg)](https://github.com/phil65/llmling-models/releases)
13
+ [![Github Contributors](https://img.shields.io/github/contributors/phil65/llmling-models)](https://github.com/phil65/llmling-models/graphs/contributors)
14
+ [![Github Discussions](https://img.shields.io/github/discussions/phil65/llmling-models)](https://github.com/phil65/llmling-models/discussions)
15
+ [![Github Forks](https://img.shields.io/github/forks/phil65/llmling-models)](https://github.com/phil65/llmling-models/forks)
16
+ [![Github Issues](https://img.shields.io/github/issues/phil65/llmling-models)](https://github.com/phil65/llmling-models/issues)
17
+ [![Github Issues](https://img.shields.io/github/issues-pr/phil65/llmling-models)](https://github.com/phil65/llmling-models/pulls)
18
+ [![Github Watchers](https://img.shields.io/github/watchers/phil65/llmling-models)](https://github.com/phil65/llmling-models/watchers)
19
+ [![Github Stars](https://img.shields.io/github/stars/phil65/llmling-models)](https://github.com/phil65/llmling-models/stars)
20
+ [![Github Repository size](https://img.shields.io/github/repo-size/phil65/llmling-models)](https://github.com/phil65/llmling-models)
21
+ [![Github last commit](https://img.shields.io/github/last-commit/phil65/llmling-models)](https://github.com/phil65/llmling-models/commits)
22
+ [![Github release date](https://img.shields.io/github/release-date/phil65/llmling-models)](https://github.com/phil65/llmling-models/releases)
23
+ [![Github language count](https://img.shields.io/github/languages/count/phil65/llmling-models)](https://github.com/phil65/llmling-models)
24
+ [![Github commits this week](https://img.shields.io/github/commit-activity/w/phil65/llmling-models)](https://github.com/phil65/llmling-models)
25
+ [![Github commits this month](https://img.shields.io/github/commit-activity/m/phil65/llmling-models)](https://github.com/phil65/llmling-models)
26
+ [![Github commits this year](https://img.shields.io/github/commit-activity/y/phil65/llmling-models)](https://github.com/phil65/llmling-models)
27
+ [![Package status](https://codecov.io/gh/phil65/llmling-models/branch/main/graph/badge.svg)](https://codecov.io/gh/phil65/llmling-models/)
28
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
29
+ [![PyUp](https://pyup.io/repos/github/phil65/llmling-models/shield.svg)](https://pyup.io/repos/github/phil65/llmling-models/)
30
+
31
+ [Read the documentation!](https://phil65.github.io/llmling-models/)
32
+
File without changes
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from duty import duty
4
+
5
+
6
+ @duty(capture=False)
7
+ def build(ctx, *args: str):
8
+ """Build a MkNodes page."""
9
+ args_str = " " + " ".join(args) if args else ""
10
+ ctx.run(f"uv run mknodes build{args_str}")
11
+
12
+
13
+ @duty(capture=False)
14
+ def serve(ctx, *args: str):
15
+ """Serve a MkNodes page."""
16
+ args_str = " " + " ".join(args) if args else ""
17
+ ctx.run(f"uv run mknodes serve{args_str}")
18
+
19
+
20
+ @duty(capture=False)
21
+ def test(ctx, *args: str):
22
+ """Serve a MkNodes page."""
23
+ args_str = " " + " ".join(args) if args else ""
24
+ ctx.run(f"uv run pytest{args_str}")
25
+
26
+
27
+ @duty(capture=False)
28
+ def clean(ctx):
29
+ """Clean all files from the Git directory except checked-in files."""
30
+ ctx.run("git clean -dfX")
31
+
32
+
33
+ @duty(capture=False)
34
+ def update(ctx):
35
+ """Update all environment packages using pip directly."""
36
+ ctx.run("uv lock --upgrade")
37
+ ctx.run("uv sync --all-extras")
38
+
39
+
40
+ @duty(capture=False)
41
+ def lint(ctx):
42
+ """Lint the code and fix issues if possible."""
43
+ ctx.run("uv run ruff check --fix --unsafe-fixes .")
44
+ ctx.run("uv run ruff format .")
45
+ ctx.run("uv run mypy src/llmling_models/")
46
+
47
+
48
+ @duty(capture=False)
49
+ def lint_check(ctx):
50
+ """Lint the code."""
51
+ ctx.run("uv run ruff check .")
52
+ ctx.run("uv run ruff format --check .")
53
+ ctx.run("uv run mypy src/llmling_models/")
54
+
55
+
56
+ @duty(capture=False)
57
+ def version(ctx, *args: str):
58
+ """Bump package version."""
59
+ args_str = " " + " ".join(args) if args else ""
60
+ ctx.run(f"hatch version{args_str}")
@@ -0,0 +1,92 @@
1
+ site_name: "LLMling-models"
2
+ site_description: "Pydantic-AI models for LLMling-agent"
3
+ repo_url: "https://github.com/phil65/llmling-models/"
4
+ site_url: https://phil65.github.io/llmling-models/
5
+ site_author: "Philipp Temminghoff"
6
+ copyright: Copyright &copy; 2024 Philipp Temminghoff
7
+
8
+ theme:
9
+ name: material
10
+ custom_dir: overrides
11
+ icon:
12
+ logo: material/graph-outline
13
+ palette:
14
+ # Palette toggle for automatic mode
15
+ - media: "(prefers-color-scheme)"
16
+ toggle:
17
+ icon: material/brightness-auto
18
+ name: Switch to light mode
19
+
20
+ # Palette toggle for light mode
21
+ - media: "(prefers-color-scheme: light)"
22
+ scheme: default
23
+ primary: red
24
+ toggle:
25
+ icon: material/brightness-7
26
+ name: Switch to dark mode
27
+
28
+ # Palette toggle for dark mode
29
+ - media: "(prefers-color-scheme: dark)"
30
+ scheme: slate
31
+ primary: red
32
+ toggle:
33
+ icon: material/brightness-4
34
+ name: Switch to system preference
35
+ features:
36
+ - announce.dismiss
37
+ - content.action.edit
38
+ - content.code.copy
39
+ - content.code.select
40
+ - content.code.annotate
41
+ - content.tooltips
42
+ # - content.tabs.link
43
+ - navigation.tracking # update URL based on current item in TOC
44
+ - navigation.path # shows breadcrumbs
45
+ - navigation.tabs # make top level tabs
46
+ - navigation.indexes # documents can be directly attached to sections (overview pages)
47
+ - navigation.footer # next/previous page buttons in footer
48
+ - navigation.top # adds back-to-top button
49
+ # - navigation.sections # top-level sections are rendered as groups
50
+ # - navigation.expand # expand all subsections in left sidebar by default
51
+ - toc.follow # makes toc follow scrolling
52
+ # - toc.integrate # integrates toc into left menu
53
+ - search.highlight
54
+ - search.suggest
55
+ # - search.share
56
+
57
+ plugins:
58
+ - search
59
+ - autorefs
60
+ - mknodes
61
+ - mkdocstrings:
62
+ default_handler: python
63
+ handlers:
64
+ python:
65
+ import:
66
+ - url: https://docs.python.org/3/objects.inv
67
+ domains: [std, py]
68
+ options:
69
+ # https://mkdocstrings.github.io/python/usage/
70
+ enable_inventory: !ENV [CI, false]
71
+ show_signature_annotations: true
72
+ show_symbol_type_toc: true
73
+ show_symbol_type_heading: true
74
+ show_root_toc_entry: false
75
+ # merge_init_into_class: true
76
+ ignore_init_summary: true
77
+ inherited_members: false
78
+ signature_crossrefs: true
79
+ separate_signature: true
80
+ line_length: 90
81
+ markdown_extensions:
82
+ - attr_list
83
+ - pymdownx.emoji
84
+ - toc:
85
+ permalink: true
86
+
87
+ extra:
88
+ social:
89
+ - icon: fontawesome/brands/github
90
+ link: https://github.com/phil65
91
+ - icon: fontawesome/brands/python
92
+ link: https://pypi.org/project/llmling-models/
@@ -0,0 +1 @@
1
+ File exists to make sure that folder exists for git
@@ -0,0 +1,246 @@
1
+ [tool.hatch.version]
2
+ source = "regex_commit"
3
+ commit_extra_args = ["-e"]
4
+ path = "src/llmling_models/__init__.py"
5
+
6
+ [build-system]
7
+ requires = ["hatchling", "hatch-regex-commit"]
8
+ build-backend = "hatchling.build"
9
+
10
+ [project]
11
+ name = "llmling-models"
12
+ description = "Pydantic-AI models for LLMling-agent"
13
+ authors = [
14
+ { name = "Philipp Temminghoff", email = "philipptemminghoff@googlemail.com" },
15
+ ]
16
+ readme = "README.md"
17
+ dynamic = ["version"]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Documentation",
28
+ "Topic :: Software Development",
29
+ "Topic :: Utilities",
30
+ "Typing :: Typed",
31
+
32
+ ]
33
+ keywords = []
34
+ requires-python = ">=3.12"
35
+ license = { file = "LICENSE" }
36
+ dependencies = [
37
+ "pydantic",
38
+ # Only add below (Copier)
39
+ "pydantic-ai>=0.0.12",
40
+ ]
41
+
42
+ [tool.uv]
43
+ default-groups = ["dev", "lint", "docs"]
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest",
48
+ "pytest-cov",
49
+ "pyreadline3",
50
+ "devtools",
51
+ # Only add below (Copier)
52
+ "pytest-asyncio>=0.25.0",
53
+ ]
54
+ benchmark = ["pyinstrument"]
55
+ lint = [
56
+ "ruff",
57
+ "mypy[faster-cache]; python_version < '3.14'",
58
+ "mypy; python_version >= '3.14'",
59
+ # Only add below (Copier)
60
+ ]
61
+ docs = [
62
+ "mkdocs-mknodes",
63
+ "mkdocs-material",
64
+ # Only add below (Copier)
65
+ ]
66
+
67
+ [project.urls]
68
+ Documentation = "https://phil65.github.io/llmling-models/"
69
+ Source = "https://github.com/phil65/llmling-models"
70
+ Issues = "https://github.com/phil65/llmling-models/issues"
71
+ Discussions = "https://github.com/phil65/llmling-models/discussions"
72
+ "Code coverage" = "https://app.codecov.io/gh/phil65/llmling-models"
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = "tests/"
76
+ log_cli = true
77
+ asyncio_default_fixture_loop_scope = "function"
78
+ log_format = "%(asctime)s %(levelname)s %(message)s"
79
+ log_date_format = "%Y-%m-%d %H:%M:%S"
80
+ asyncio_mode = "auto"
81
+
82
+ [tool.coverage.report]
83
+ exclude_lines = [
84
+ "pragma: no cover",
85
+ "if TYPE_CHECKING:",
86
+ "@overload",
87
+ "except ImportError",
88
+ 'if __name__ == "__main__":',
89
+ ]
90
+
91
+ [tool.mypy]
92
+ python_version = "3.12"
93
+ disable_error_code = ["misc", "import"]
94
+ pretty = true
95
+ check_untyped_defs = true
96
+ exclude = ['venv/', '.venv/', 'tests/']
97
+ plugins = ["pydantic.mypy"]
98
+
99
+ [tool.ruff]
100
+ line-length = 90
101
+ extend-exclude = ['docs']
102
+ target-version = "py312"
103
+
104
+ [tool.ruff.lint]
105
+ select = [
106
+ "A", # Flake8-builtins
107
+ # "ANN", # Flake8-Annotations
108
+ # "ASYNC", # Flake8-Async
109
+ # "ARG", # # Flake8-Unused-Arguments
110
+ "B", # flake8-bugbear
111
+ "BLE", # Flake8-blind-except
112
+ "C",
113
+ "C4", # flake8-comprehensions
114
+ # "C90", # MCCabe
115
+ "COM", # Flake8-commas
116
+ # "CPY", # Copyright-related rules
117
+ "D", # PyDocStyle
118
+ # "DTZ", # Flake8- Datetimez
119
+ "E", # PyCodeStyle Error
120
+ "EM", # flake8-errmsg
121
+ # "ERA", # Eradicate
122
+ "EXE", # flake8-executable
123
+ "F", # PyFlakes
124
+ "FA", # flake8-future-annotations
125
+ # "FBT", # flake8-boolean-trap
126
+ # "FIX", # flake8-fixme
127
+ "FLY", # flynt
128
+ "G", # flake8-logging-format
129
+ "I", # ISort
130
+ "ICN", # Flake8-import-conventions
131
+ "INP", # flake8-no-pep420
132
+ "INT", # flake8-gettext
133
+ "ISC", # flake8-implicit-str-concat
134
+ "N", # pep8-naming
135
+ # "NPY", # numpy-specific rules
136
+ # "PD", # pandas-vet
137
+ "PERF", # perflint
138
+ # "PGH", # pygrep-hooks
139
+ "PIE", # flake8-pie
140
+ "PLE", # PyLint Error
141
+ "PLC", # PyLint convention
142
+ # "PLW", # PyLint Warning
143
+ "PLR", # PyLint refactor
144
+ "PT", # flake8-pytest-style
145
+ "PTH", # flake8-use-pathlib
146
+ "PYI", # flake8-pyi
147
+ "Q", # flake8-quotes
148
+ "RET", # flake8-return
149
+ "RSE", # flake8-raise
150
+ "RUF", # ruff-specific rules
151
+ # "S", # flake8-bandit
152
+ "SIM", # flake8-simplify
153
+ "SLF", # flake8-self
154
+ "SLOT", # flake8-slots
155
+ # "T",
156
+ # "TD", # flake8-todos
157
+ "T10", # flake8-debugger
158
+ # "T20", # flake8-print
159
+ "TC", # flake8-type-checking
160
+ "TID", # flake8-tidy-imports
161
+ "TRY", # tryceratops
162
+ "UP", # PyUpgrade
163
+ "W", # PyCodeStyle warning
164
+ "YTT", # flake8-2020
165
+ ]
166
+ ignore = [
167
+ "C408", # Unnecessary {obj_type} call (rewrite as a literal)
168
+ "B905", # zip() without an explicit strict= parameter
169
+ "C901", # {name} is too complex ({complexity} > {max_complexity})
170
+ "COM812",
171
+ # "CPY001", # Missing copyright notice at top of file
172
+ "D100", # Missing docstring in public module
173
+ "D101", # Missing docstring in public class
174
+ "D102", # Missing docstring in public method
175
+ "D103", # Missing docstring in public function
176
+ "D104", # Missing docstring in public package
177
+ "D105", # Missing docstring in magic method
178
+ "D106", # Missing docstring in public nested class
179
+ "D107", # Missing docstring in __init__
180
+ "D203", # 1 blank line required before class docstring
181
+ "D204", # 1 blank line required after class docstring
182
+ "D213", # Multi-line docstring summary should start at the second line
183
+ "D215", # Section underline is over-indented ("{name}")
184
+ "D400", # First line should end with a period
185
+ "D401", # First line of docstring should be in imperative mood: "{first_line}"
186
+ "D404", # First word of the docstring should not be "This"
187
+ "D406", # Section name should end with a newline ("{name}")
188
+ "D407", # Missing dashed underline after section ("{name}")
189
+ "D408", # Section underline should be in the line following the section's name ("{name}")
190
+ "D409", # Section underline should match the length of its name ("{name}")
191
+ "D413", # Missing blank line after last section ("{name}")
192
+ "ISC001",
193
+ "PLR0912", # Too many branches
194
+ "PLR0913", # Too many arguments to function call
195
+ "PLR0915", # Too many statements
196
+ # "PLR2004", # Magic values instead of named consts
197
+ "SLF001", # Private member accessed
198
+ "TRY003", # Avoid specifying long messages outside the exception class
199
+ ]
200
+
201
+ [tool.ruff.lint.flake8-quotes]
202
+ docstring-quotes = "double"
203
+
204
+ [tool.ruff.format]
205
+ # Enable preview style formatting.
206
+ preview = true
207
+
208
+ [tool.ruff.lint.isort]
209
+ lines-after-imports = 2
210
+ # lines-between-types = 1
211
+ # atomic = true
212
+ force-sort-within-sections = true
213
+ combine-as-imports = true
214
+
215
+ [tool.ruff.lint.per-file-ignores]
216
+ "__init__.py" = ["E402", "I001"]
217
+ "scripts/*" = ["INP001"]
218
+
219
+ [tool.pyright]
220
+ pythonVersion = "3.12"
221
+ pythonPlatform = "All"
222
+ typeCheckingMode = "basic"
223
+ deprecateTypingAliases = true
224
+ reportMissingTypeStubs = false
225
+ reportUnusedCallResult = false
226
+ reportUnknownVariableType = false
227
+ reportAny = false
228
+ reportImplicitOverride = false
229
+ reportUnusedFunction = false
230
+ reportImplicitStringConcatenation = false
231
+ reportIgnoreCommentWithoutRule = false
232
+ reportUnannotatedClassAttribute = false
233
+ reportSelfClsParameterName = false
234
+ reportPrivateImportUsage = false
235
+
236
+ [tool.mknodes]
237
+ allowed-commit-types = [
238
+ "fix",
239
+ "feat",
240
+ "refactor",
241
+ "docs",
242
+ "test",
243
+ "build",
244
+ "chore",
245
+ ]
246
+ docstring-style = "google"
@@ -0,0 +1,11 @@
1
+ __version__ = "0.0.2"
2
+
3
+
4
+ from llmling_models.base import PydanticModel
5
+ from llmling_models.multi import MultiModel, RandomMultiModel
6
+
7
+ __all__ = [
8
+ "MultiModel",
9
+ "PydanticModel",
10
+ "RandomMultiModel",
11
+ ]
@@ -0,0 +1,16 @@
1
+ """Base class for YAML-configurable pydantic-ai models."""
2
+
3
+ from abc import ABC
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+ from pydantic_ai.models import Model
7
+
8
+
9
+ class PydanticModel(Model, BaseModel, ABC):
10
+ """Base for models that can be configured via YAML."""
11
+
12
+ model_config = ConfigDict(
13
+ arbitrary_types_allowed=True,
14
+ extra="forbid",
15
+ use_attribute_docstrings=True,
16
+ )
@@ -0,0 +1,17 @@
1
+ """Logging configuration for llmling_models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+
8
+ def get_logger(name: str) -> logging.Logger:
9
+ """Get a logger for the given name.
10
+
11
+ Args:
12
+ name: The name of the logger, will be prefixed with 'llmling_agent.'
13
+
14
+ Returns:
15
+ A logger instance
16
+ """
17
+ return logging.getLogger(f"llmling_models.{name}")
@@ -0,0 +1,227 @@
1
+ """Multi-model implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import TYPE_CHECKING, Any, Literal
7
+
8
+ from pydantic import Field, model_validator
9
+ from pydantic_ai.models import AgentModel, KnownModelName, Model, infer_model
10
+
11
+ from llmling_models.base import PydanticModel
12
+ from llmling_models.log import get_logger
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from pydantic_ai.messages import Message, ModelAnyResponse
17
+ from pydantic_ai.tools import ToolDefinition
18
+
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class MultiModel(PydanticModel):
24
+ """Base for model configurations that combine multiple language models.
25
+
26
+ This provides the base interface for YAML-configurable multi-model setups,
27
+ allowing configuration of multiple models through LLMling's config system.
28
+ """
29
+
30
+ type: str
31
+ """Discriminator field for multi-model types"""
32
+
33
+ models: list[KnownModelName | Model] = Field(default_factory=list)
34
+ """"List of models to use"."""
35
+
36
+ @property
37
+ def available_models(self) -> list[Model]:
38
+ """Convert model names/instances to pydantic-ai Model instances."""
39
+ return [
40
+ model if isinstance(model, Model) else infer_model(model) # type: ignore[arg-type]
41
+ for model in self.models
42
+ ]
43
+
44
+ @model_validator(mode="after")
45
+ def validate_models(self) -> MultiModel:
46
+ """Validate model configuration."""
47
+ if not self.models:
48
+ msg = "At least one model must be provided"
49
+ raise ValueError(msg)
50
+ return self
51
+
52
+
53
+ class RandomMultiModel(MultiModel):
54
+ """Randomly selects from configured models.
55
+
56
+ Example YAML configuration:
57
+ ```yaml
58
+ model:
59
+ type: random
60
+ models:
61
+ - openai:gpt-4
62
+ - openai:gpt-3.5-turbo
63
+ ```
64
+ """
65
+
66
+ type: Literal["random"] = "random"
67
+
68
+ @model_validator(mode="after")
69
+ def validate_models(self) -> RandomMultiModel:
70
+ """Validate model configuration."""
71
+ if not self.models:
72
+ msg = "At least one model must be provided"
73
+ raise ValueError(msg)
74
+ return self
75
+
76
+ def name(self) -> str:
77
+ """Get descriptive model name."""
78
+ return f"multi-random({len(self.models)})"
79
+
80
+ async def agent_model(
81
+ self,
82
+ *,
83
+ function_tools: list[ToolDefinition],
84
+ allow_text_result: bool,
85
+ result_tools: list[ToolDefinition],
86
+ ) -> AgentModel:
87
+ """Create agent model that randomly selects from available models."""
88
+ return RandomAgentModel(
89
+ models=self.available_models,
90
+ function_tools=function_tools,
91
+ allow_text_result=allow_text_result,
92
+ result_tools=result_tools,
93
+ )
94
+
95
+
96
+ class RandomAgentModel(AgentModel):
97
+ """AgentModel that randomly selects from available models."""
98
+
99
+ def __init__(
100
+ self,
101
+ models: list[Model],
102
+ function_tools: list[ToolDefinition],
103
+ allow_text_result: bool,
104
+ result_tools: list[ToolDefinition],
105
+ ) -> None:
106
+ """Initialize with list of models."""
107
+ if not models:
108
+ msg = "At least one model must be provided"
109
+ raise ValueError(msg)
110
+ self.models = models
111
+ self.function_tools = function_tools
112
+ self.allow_text_result = allow_text_result
113
+ self.result_tools = result_tools
114
+ self._initialized_models: list[AgentModel] | None = None
115
+
116
+ async def _initialize_models(self) -> list[AgentModel]:
117
+ """Initialize all agent models."""
118
+ if self._initialized_models is None:
119
+ self._initialized_models = []
120
+ for model in self.models:
121
+ agent_model = await model.agent_model(
122
+ function_tools=self.function_tools,
123
+ allow_text_result=self.allow_text_result,
124
+ result_tools=self.result_tools,
125
+ )
126
+ self._initialized_models.append(agent_model)
127
+ return self._initialized_models
128
+
129
+ async def request(
130
+ self,
131
+ messages: list[Message],
132
+ ) -> tuple[ModelAnyResponse, Any]:
133
+ """Make request using randomly selected model."""
134
+ models = await self._initialize_models()
135
+ selected = random.choice(models)
136
+ logger.debug("Selected model: %s", selected)
137
+ return await selected.request(messages)
138
+
139
+
140
+ class FallbackMultiModel(MultiModel):
141
+ """Tries models in sequence until one succeeds.
142
+
143
+ Example YAML configuration:
144
+ ```yaml
145
+ model:
146
+ type: fallback
147
+ models:
148
+ - openai:gpt-4 # Try this first
149
+ - openai:gpt-3.5-turbo # Fall back to this if gpt-4 fails
150
+ - ollama:llama2 # Last resort
151
+ ```
152
+ """
153
+
154
+ type: Literal["fallback"] = "fallback"
155
+
156
+ def name(self) -> str:
157
+ """Get descriptive model name."""
158
+ return f"multi-fallback({len(self.models)})"
159
+
160
+ async def agent_model(
161
+ self,
162
+ *,
163
+ function_tools: list[ToolDefinition],
164
+ allow_text_result: bool,
165
+ result_tools: list[ToolDefinition],
166
+ ) -> AgentModel:
167
+ """Create agent model that implements fallback strategy."""
168
+ return FallbackAgentModel(
169
+ models=self.available_models,
170
+ function_tools=function_tools,
171
+ allow_text_result=allow_text_result,
172
+ result_tools=result_tools,
173
+ )
174
+
175
+
176
+ class FallbackAgentModel(AgentModel):
177
+ """AgentModel that implements fallback strategy."""
178
+
179
+ def __init__(
180
+ self,
181
+ models: list[Model],
182
+ function_tools: list[ToolDefinition],
183
+ allow_text_result: bool,
184
+ result_tools: list[ToolDefinition],
185
+ ) -> None:
186
+ """Initialize with ordered list of models."""
187
+ if not models:
188
+ msg = "At least one model must be provided"
189
+ raise ValueError(msg)
190
+ self.models = models
191
+ self.function_tools = function_tools
192
+ self.allow_text_result = allow_text_result
193
+ self.result_tools = result_tools
194
+ self._initialized_models: list[AgentModel] | None = None
195
+
196
+ async def _initialize_models(self) -> list[AgentModel]:
197
+ """Initialize all agent models."""
198
+ if self._initialized_models is None:
199
+ self._initialized_models = []
200
+ for model in self.models:
201
+ agent_model = await model.agent_model(
202
+ function_tools=self.function_tools,
203
+ allow_text_result=self.allow_text_result,
204
+ result_tools=self.result_tools,
205
+ )
206
+ self._initialized_models.append(agent_model)
207
+ return self._initialized_models
208
+
209
+ async def request(
210
+ self,
211
+ messages: list[Message],
212
+ ) -> tuple[ModelAnyResponse, Any]:
213
+ """Try each model in sequence until one succeeds."""
214
+ models = await self._initialize_models()
215
+ last_error = None
216
+
217
+ for model in models:
218
+ try:
219
+ logger.debug("Trying model: %s", model)
220
+ return await model.request(messages)
221
+ except Exception as e: # noqa: BLE001
222
+ last_error = e
223
+ logger.debug("Model %s failed: %s", model, e)
224
+ continue
225
+
226
+ msg = f"All models failed. Last error: {last_error}"
227
+ raise RuntimeError(msg) from last_error
File without changes
@@ -0,0 +1,68 @@
1
+ from typing import Annotated, Literal
2
+
3
+ from pydantic import Field
4
+ from pydantic_ai.models import AgentModel, KnownModelName, Model, infer_model
5
+ from pydantic_ai.models.test import TestModel
6
+ from pydantic_ai.tools import ToolDefinition
7
+
8
+ from llmling_models.base import PydanticModel
9
+ from llmling_models.multi import RandomMultiModel
10
+
11
+
12
+ class _TestModelWrapper(PydanticModel):
13
+ """Wrapper for TestModel."""
14
+
15
+ type: Literal["test"] = "test"
16
+ model: TestModel
17
+
18
+ def name(self) -> str:
19
+ """Get model name."""
20
+ return "test"
21
+
22
+ async def agent_model(
23
+ self,
24
+ *,
25
+ function_tools: list[ToolDefinition],
26
+ allow_text_result: bool,
27
+ result_tools: list[ToolDefinition],
28
+ ) -> AgentModel:
29
+ """Create agent model implementation."""
30
+ return await self.model.agent_model(
31
+ function_tools=function_tools,
32
+ allow_text_result=allow_text_result,
33
+ result_tools=result_tools,
34
+ )
35
+
36
+
37
+ class StringModel(PydanticModel):
38
+ """Wrapper for string model names."""
39
+
40
+ type: Literal["string"] = "string"
41
+ identifier: KnownModelName # renamed from name
42
+
43
+ async def agent_model(
44
+ self,
45
+ *,
46
+ function_tools: list[ToolDefinition],
47
+ allow_text_result: bool,
48
+ result_tools: list[ToolDefinition],
49
+ ) -> AgentModel:
50
+ """Create agent model from string name."""
51
+ model = infer_model(self.identifier) # type: ignore
52
+ return await model.agent_model(
53
+ function_tools=function_tools,
54
+ allow_text_result=allow_text_result,
55
+ result_tools=result_tools,
56
+ )
57
+
58
+ def name(self) -> str:
59
+ """Get model name."""
60
+ return str(self.identifier)
61
+
62
+
63
+ type ModelInput = str | KnownModelName | Model | PydanticModel
64
+ """Type for internal model handling (after validation)."""
65
+
66
+ AnyModel = Annotated[
67
+ StringModel | RandomMultiModel | _TestModelWrapper, Field(discriminator="type")
68
+ ]
@@ -0,0 +1,3 @@
1
+ from pathlib import Path
2
+
3
+ TESTS_DIR = Path(__file__).parent
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,109 @@
1
+ """Tests for MultiModel implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import ValidationError
6
+ from pydantic_ai import Agent, Tool
7
+ from pydantic_ai.models.test import TestModel
8
+ import pytest
9
+
10
+ from llmling_models.multi import RandomMultiModel
11
+ from llmling_models.types import _TestModelWrapper
12
+
13
+
14
+ @pytest.fixture
15
+ def test_models() -> tuple[TestModel, TestModel]:
16
+ """Create two test models with different responses."""
17
+ return (
18
+ TestModel(custom_result_text="Response from Model 1"),
19
+ TestModel(custom_result_text="Response from Model 2"),
20
+ )
21
+
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_random_model_basic(test_models: tuple[TestModel, TestModel]) -> None:
25
+ """Test basic RandomMultiModel functionality with pydantic-ai Agent."""
26
+ model1, model2 = test_models
27
+ random_model = RandomMultiModel(
28
+ type="random",
29
+ models=[
30
+ _TestModelWrapper(type="test", model=model1),
31
+ _TestModelWrapper(type="test", model=model2),
32
+ ],
33
+ )
34
+
35
+ # Create a simple agent with our random model
36
+ agent = Agent(model=random_model)
37
+
38
+ # Run multiple times to collect different responses
39
+ responses = set()
40
+ for _ in range(10):
41
+ result = await agent.run("Test prompt")
42
+ responses.add(result.data)
43
+
44
+ # Verify we get both responses
45
+ assert len(responses) == 2 # noqa: PLR2004
46
+ assert "Response from Model 1" in responses
47
+ assert "Response from Model 2" in responses
48
+
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_random_model_with_tools(test_models: tuple[TestModel, TestModel]) -> None:
52
+ """Test RandomMultiModel with tool usage."""
53
+ model1, model2 = test_models
54
+ random_model = RandomMultiModel(
55
+ type="random",
56
+ models=[
57
+ _TestModelWrapper(type="test", model=model1),
58
+ _TestModelWrapper(type="test", model=model2),
59
+ ],
60
+ )
61
+
62
+ # Create test tool
63
+ async def test_tool(text: str) -> str:
64
+ return f"Processed: {text}"
65
+
66
+ # Create agent with tool
67
+ agent = Agent(
68
+ model=random_model,
69
+ tools=[Tool(test_tool, takes_ctx=False)],
70
+ )
71
+
72
+ # Run multiple times
73
+ responses = set()
74
+ for _ in range(10):
75
+ result = await agent.run("Use the test tool")
76
+ responses.add(result.data)
77
+
78
+ assert len(responses) == 2 # noqa: PLR2004
79
+
80
+
81
+ def test_random_model_validation() -> None:
82
+ """Test RandomMultiModel validation."""
83
+ # Test empty models list
84
+ with pytest.raises(ValueError): # noqa: PT011
85
+ RandomMultiModel(type="random", models=[])
86
+
87
+ # Test invalid model name
88
+ with pytest.raises(ValidationError):
89
+ RandomMultiModel(type="random", models=["invalid_model"]) # type: ignore
90
+
91
+
92
+ def test_yaml_loading() -> None:
93
+ """Test loading RandomMultiModel from YAML configuration."""
94
+ import yaml
95
+
96
+ config = """
97
+ type: random
98
+ models:
99
+ - test
100
+ - openai:gpt-4
101
+ """
102
+
103
+ data = yaml.safe_load(config)
104
+ model = RandomMultiModel.model_validate(data)
105
+
106
+ assert model.type == "random"
107
+ assert len(model.models) == 2 # noqa: PLR2004
108
+ assert "test" in model.models
109
+ assert "openai:gpt-4" in model.models