not-again-ai 0.14.0__tar.gz → 0.16.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.
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/PKG-INFO +24 -40
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/README.md +4 -18
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/pyproject.toml +49 -42
- not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/__init__.py +4 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/interface.py +32 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/providers/ollama_api.py +227 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/providers/openai_api.py +290 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/types.py +145 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/embedding/__init__.py +4 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/embedding/interface.py +28 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/embedding/providers/ollama_api.py +87 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/embedding/providers/openai_api.py +126 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/embedding/types.py +23 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/prompting/__init__.py +3 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/prompting/compile_prompt.py +125 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/prompting/interface.py +46 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/prompting/providers/openai_tiktoken.py +122 -0
- not_again_ai-0.16.0/src/not_again_ai/llm/prompting/types.py +43 -0
- not_again_ai-0.14.0/src/not_again_ai/llm/gh_models/azure_ai_client.py +0 -20
- not_again_ai-0.14.0/src/not_again_ai/llm/gh_models/chat_completion.py +0 -81
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/chat_completion.py +0 -339
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/context_management.py +0 -70
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/embeddings.py +0 -62
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/openai_client.py +0 -78
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/prompts.py +0 -191
- not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/tokens.py +0 -184
- not_again_ai-0.14.0/src/not_again_ai/local_llm/__init__.py +0 -27
- not_again_ai-0.14.0/src/not_again_ai/local_llm/chat_completion.py +0 -105
- not_again_ai-0.14.0/src/not_again_ai/local_llm/huggingface/chat_completion.py +0 -59
- not_again_ai-0.14.0/src/not_again_ai/local_llm/huggingface/helpers.py +0 -23
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/__init__.py +0 -0
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/chat_completion.py +0 -111
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/model_mapping.py +0 -17
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/ollama_client.py +0 -24
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/service.py +0 -81
- not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/tokens.py +0 -104
- not_again_ai-0.14.0/src/not_again_ai/local_llm/prompts.py +0 -38
- not_again_ai-0.14.0/src/not_again_ai/local_llm/tokens.py +0 -90
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/LICENSE +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/file_system.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/parallel.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/data/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/data/web.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/llm/__init__.py +0 -0
- {not_again_ai-0.14.0/src/not_again_ai/llm/gh_models → not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/providers}/__init__.py +0 -0
- {not_again_ai-0.14.0/src/not_again_ai/llm/openai_api → not_again_ai-0.16.0/src/not_again_ai/llm/embedding/providers}/__init__.py +0 -0
- {not_again_ai-0.14.0/src/not_again_ai/local_llm/huggingface → not_again_ai-0.16.0/src/not_again_ai/llm/prompting/providers}/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/py.typed +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/statistics/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/statistics/dependence.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/__init__.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/barplots.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/distributions.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/scatterplot.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/time_series.py +0 -0
- {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/utils.py +0 -0
@@ -1,12 +1,11 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: not-again-ai
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.16.0
|
4
4
|
Summary: Designed to once and for all collect all the little things that come up over and over again in AI projects and put them in one place.
|
5
|
-
Home-page: https://github.com/DaveCoDev/not-again-ai
|
6
5
|
License: MIT
|
7
6
|
Author: DaveCoDev
|
8
7
|
Author-email: dave.co.dev@gmail.com
|
9
|
-
Requires-Python: >=3.11
|
8
|
+
Requires-Python: >=3.11, <3.13
|
10
9
|
Classifier: Development Status :: 3 - Alpha
|
11
10
|
Classifier: Intended Audience :: Developers
|
12
11
|
Classifier: Intended Audience :: Science/Research
|
@@ -19,26 +18,25 @@ Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Typing :: Typed
|
20
19
|
Provides-Extra: data
|
21
20
|
Provides-Extra: llm
|
22
|
-
Provides-Extra: local-llm
|
23
21
|
Provides-Extra: statistics
|
24
22
|
Provides-Extra: viz
|
25
|
-
Requires-Dist: azure-
|
26
|
-
Requires-Dist:
|
27
|
-
Requires-Dist:
|
28
|
-
Requires-Dist:
|
29
|
-
Requires-Dist:
|
30
|
-
Requires-Dist:
|
31
|
-
Requires-Dist:
|
32
|
-
Requires-Dist:
|
33
|
-
Requires-Dist: pydantic (>=2.
|
34
|
-
Requires-Dist: pytest-playwright (>=0.
|
35
|
-
Requires-Dist: python-liquid (>=1.12
|
36
|
-
Requires-Dist: scikit-learn (>=1.
|
37
|
-
Requires-Dist: scipy (>=1.
|
38
|
-
Requires-Dist: seaborn (>=0.13
|
39
|
-
Requires-Dist: tiktoken (>=0.8
|
40
|
-
|
41
|
-
Project-URL:
|
23
|
+
Requires-Dist: azure-identity (>=1.19) ; extra == "llm"
|
24
|
+
Requires-Dist: loguru (>=0.7)
|
25
|
+
Requires-Dist: numpy (>=2.2) ; extra == "statistics"
|
26
|
+
Requires-Dist: numpy (>=2.2) ; extra == "viz"
|
27
|
+
Requires-Dist: ollama (>=0.4) ; extra == "llm"
|
28
|
+
Requires-Dist: openai (>=1) ; extra == "llm"
|
29
|
+
Requires-Dist: pandas (>=2.2) ; extra == "viz"
|
30
|
+
Requires-Dist: playwright (>=1.49) ; extra == "data"
|
31
|
+
Requires-Dist: pydantic (>=2.10)
|
32
|
+
Requires-Dist: pytest-playwright (>=0.7) ; extra == "data"
|
33
|
+
Requires-Dist: python-liquid (>=1.12) ; extra == "llm"
|
34
|
+
Requires-Dist: scikit-learn (>=1.6) ; extra == "statistics"
|
35
|
+
Requires-Dist: scipy (>=1.15) ; extra == "statistics"
|
36
|
+
Requires-Dist: seaborn (>=0.13) ; extra == "viz"
|
37
|
+
Requires-Dist: tiktoken (>=0.8) ; extra == "llm"
|
38
|
+
Project-URL: Documentation, https://davecodev.github.io/not-again-ai/
|
39
|
+
Project-URL: Homepage, https://github.com/DaveCoDev/not-again-ai
|
42
40
|
Project-URL: Repository, https://github.com/DaveCoDev/not-again-ai
|
43
41
|
Description-Content-Type: text/markdown
|
44
42
|
|
@@ -68,11 +66,9 @@ Requires: Python 3.11, or 3.12
|
|
68
66
|
Install the entire package from [PyPI](https://pypi.org/project/not-again-ai/) with:
|
69
67
|
|
70
68
|
```bash
|
71
|
-
$ pip install not_again_ai[llm,
|
69
|
+
$ pip install not_again_ai[data,llm,statistics,viz]
|
72
70
|
```
|
73
71
|
|
74
|
-
Note that local LLM requires separate installations and will not work out of the box due to how hardware dependent it is. Be sure to check the [notebooks](notebooks/local_llm/) for more details.
|
75
|
-
|
76
72
|
The package is split into subpackages, so you can install only the parts you need.
|
77
73
|
|
78
74
|
### Base
|
@@ -93,16 +89,7 @@ The package is split into subpackages, so you can install only the parts you nee
|
|
93
89
|
1. Using AOAI requires using Entra ID authentication. See https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity for how to set this up for your AOAI deployment.
|
94
90
|
* Requires the correct role assigned to your user account and being signed into the Azure CLI.
|
95
91
|
1. (Optional) Set the `AZURE_OPENAI_ENDPOINT` environment variable.
|
96
|
-
1.
|
97
|
-
1. Get a Personal Access Token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable. The token does not need any permissions.
|
98
|
-
1. Check the [Github Marketplace](https://github.com/marketplace/models) to see which models are available.
|
99
|
-
|
100
|
-
|
101
|
-
### Local LLM
|
102
|
-
1. `pip install not_again_ai[llm,local_llm]`
|
103
|
-
1. Some HuggingFace transformers tokenizers are gated behind access requests. If you wish to use these, you will need to request access from HuggingFace on the model card.
|
104
|
-
* Then set the `HF_TOKEN` environment variable to your HuggingFace API token which can be found here: https://huggingface.co/settings/tokens
|
105
|
-
1. If you wish to use Ollama:
|
92
|
+
1. If you wish to use Ollama:
|
106
93
|
1. Follow the instructions at https://github.com/ollama/ollama to install Ollama for your system.
|
107
94
|
1. (Optional) [Add Ollama as a startup service (recommended)](https://github.com/ollama/ollama/blob/main/docs/linux.md#adding-ollama-as-a-startup-service-recommended)
|
108
95
|
1. (Optional) To make the Ollama service accessible on your local network from a Linux server, add the following to the `/etc/systemd/system/ollama.service` file which will make Ollama available at `http://<local_address>:11434`:
|
@@ -112,7 +99,6 @@ The package is split into subpackages, so you can install only the parts you nee
|
|
112
99
|
Environment="OLLAMA_HOST=0.0.0.0"
|
113
100
|
```
|
114
101
|
1. It is recommended to always have the latest version of Ollama. To update Ollama check the [docs](https://github.com/ollama/ollama/blob/main/docs/). The command for Linux is: `curl -fsSL https://ollama.com/install.sh | sh`
|
115
|
-
1. HuggingFace transformers and other requirements are hardware dependent so for providers other than Ollama, this only installs some generic dependencies. Check the [notebooks](notebooks/local_llm/) for more details on what is available and how to install it.
|
116
102
|
|
117
103
|
|
118
104
|
### Statistics
|
@@ -156,10 +142,8 @@ $ poetry update
|
|
156
142
|
|
157
143
|
To install all dependencies (with all extra dependencies) into an isolated virtual environment:
|
158
144
|
|
159
|
-
> Append `--sync` to uninstall dependencies that are no longer in use from the virtual environment.
|
160
|
-
|
161
145
|
```bash
|
162
|
-
$ poetry
|
146
|
+
$ poetry sync --all-extras
|
163
147
|
```
|
164
148
|
|
165
149
|
To [activate](https://python-poetry.org/docs/basic-usage#activating-the-virtual-environment) the
|
@@ -215,7 +199,7 @@ Automated code quality checks are performed using
|
|
215
199
|
environments and run commands based on [`noxfile.py`](./noxfile.py) for unit testing, PEP 8 style
|
216
200
|
guide checking, type checking and documentation generation.
|
217
201
|
|
218
|
-
> Note: `nox` is installed into the virtual environment automatically by the `poetry
|
202
|
+
> Note: `nox` is installed into the virtual environment automatically by the `poetry sync`
|
219
203
|
> command above. Run `poetry shell` to activate the virtual environment.
|
220
204
|
|
221
205
|
To run all default sessions:
|
@@ -24,11 +24,9 @@ Requires: Python 3.11, or 3.12
|
|
24
24
|
Install the entire package from [PyPI](https://pypi.org/project/not-again-ai/) with:
|
25
25
|
|
26
26
|
```bash
|
27
|
-
$ pip install not_again_ai[llm,
|
27
|
+
$ pip install not_again_ai[data,llm,statistics,viz]
|
28
28
|
```
|
29
29
|
|
30
|
-
Note that local LLM requires separate installations and will not work out of the box due to how hardware dependent it is. Be sure to check the [notebooks](notebooks/local_llm/) for more details.
|
31
|
-
|
32
30
|
The package is split into subpackages, so you can install only the parts you need.
|
33
31
|
|
34
32
|
### Base
|
@@ -49,16 +47,7 @@ The package is split into subpackages, so you can install only the parts you nee
|
|
49
47
|
1. Using AOAI requires using Entra ID authentication. See https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity for how to set this up for your AOAI deployment.
|
50
48
|
* Requires the correct role assigned to your user account and being signed into the Azure CLI.
|
51
49
|
1. (Optional) Set the `AZURE_OPENAI_ENDPOINT` environment variable.
|
52
|
-
1.
|
53
|
-
1. Get a Personal Access Token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable. The token does not need any permissions.
|
54
|
-
1. Check the [Github Marketplace](https://github.com/marketplace/models) to see which models are available.
|
55
|
-
|
56
|
-
|
57
|
-
### Local LLM
|
58
|
-
1. `pip install not_again_ai[llm,local_llm]`
|
59
|
-
1. Some HuggingFace transformers tokenizers are gated behind access requests. If you wish to use these, you will need to request access from HuggingFace on the model card.
|
60
|
-
* Then set the `HF_TOKEN` environment variable to your HuggingFace API token which can be found here: https://huggingface.co/settings/tokens
|
61
|
-
1. If you wish to use Ollama:
|
50
|
+
1. If you wish to use Ollama:
|
62
51
|
1. Follow the instructions at https://github.com/ollama/ollama to install Ollama for your system.
|
63
52
|
1. (Optional) [Add Ollama as a startup service (recommended)](https://github.com/ollama/ollama/blob/main/docs/linux.md#adding-ollama-as-a-startup-service-recommended)
|
64
53
|
1. (Optional) To make the Ollama service accessible on your local network from a Linux server, add the following to the `/etc/systemd/system/ollama.service` file which will make Ollama available at `http://<local_address>:11434`:
|
@@ -68,7 +57,6 @@ The package is split into subpackages, so you can install only the parts you nee
|
|
68
57
|
Environment="OLLAMA_HOST=0.0.0.0"
|
69
58
|
```
|
70
59
|
1. It is recommended to always have the latest version of Ollama. To update Ollama check the [docs](https://github.com/ollama/ollama/blob/main/docs/). The command for Linux is: `curl -fsSL https://ollama.com/install.sh | sh`
|
71
|
-
1. HuggingFace transformers and other requirements are hardware dependent so for providers other than Ollama, this only installs some generic dependencies. Check the [notebooks](notebooks/local_llm/) for more details on what is available and how to install it.
|
72
60
|
|
73
61
|
|
74
62
|
### Statistics
|
@@ -112,10 +100,8 @@ $ poetry update
|
|
112
100
|
|
113
101
|
To install all dependencies (with all extra dependencies) into an isolated virtual environment:
|
114
102
|
|
115
|
-
> Append `--sync` to uninstall dependencies that are no longer in use from the virtual environment.
|
116
|
-
|
117
103
|
```bash
|
118
|
-
$ poetry
|
104
|
+
$ poetry sync --all-extras
|
119
105
|
```
|
120
106
|
|
121
107
|
To [activate](https://python-poetry.org/docs/basic-usage#activating-the-virtual-environment) the
|
@@ -171,7 +157,7 @@ Automated code quality checks are performed using
|
|
171
157
|
environments and run commands based on [`noxfile.py`](./noxfile.py) for unit testing, PEP 8 style
|
172
158
|
guide checking, type checking and documentation generation.
|
173
159
|
|
174
|
-
> Note: `nox` is installed into the virtual environment automatically by the `poetry
|
160
|
+
> Note: `nox` is installed into the virtual environment automatically by the `poetry sync`
|
175
161
|
> command above. Run `poetry shell` to activate the virtual environment.
|
176
162
|
|
177
163
|
To run all default sessions:
|
@@ -1,8 +1,10 @@
|
|
1
|
-
[
|
1
|
+
[project]
|
2
2
|
name = "not-again-ai"
|
3
|
-
version = "0.
|
3
|
+
version = "0.16.0"
|
4
4
|
description = "Designed to once and for all collect all the little things that come up over and over again in AI projects and put them in one place."
|
5
|
-
authors = [
|
5
|
+
authors = [
|
6
|
+
{ name = "DaveCoDev", email = "dave.co.dev@gmail.com" }
|
7
|
+
]
|
6
8
|
license = "MIT"
|
7
9
|
readme = "README.md"
|
8
10
|
repository = "https://github.com/DaveCoDev/not-again-ai"
|
@@ -19,41 +21,47 @@ classifiers = [
|
|
19
21
|
"Programming Language :: Python :: 3.12",
|
20
22
|
"Typing :: Typed",
|
21
23
|
]
|
24
|
+
requires-python = ">=3.11, <3.13"
|
25
|
+
dependencies = [
|
26
|
+
"loguru>=0.7",
|
27
|
+
"pydantic>=2.10"
|
28
|
+
]
|
29
|
+
|
30
|
+
[project.urls]
|
31
|
+
Homepage = "https://github.com/DaveCoDev/not-again-ai"
|
32
|
+
Documentation = "https://davecodev.github.io/not-again-ai/"
|
33
|
+
Repository = "https://github.com/DaveCoDev/not-again-ai"
|
34
|
+
|
35
|
+
[tool.poetry]
|
36
|
+
requires-poetry = ">=2.0.1"
|
37
|
+
|
38
|
+
[tool.poetry.requires-plugins]
|
39
|
+
poetry-plugin-export = ">=1.8"
|
22
40
|
|
23
|
-
[
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
transformers = { version = "^4.45", optional = true }
|
48
|
-
|
49
|
-
[tool.poetry.extras]
|
50
|
-
data = ["pytest-playwright"]
|
51
|
-
llm = ["azure-ai-inference", "azure-identity", "openai", "python-liquid", "tiktoken"]
|
52
|
-
local_llm = ["jinja2", "ollama", "transformers"]
|
53
|
-
statistics = ["numpy", "scikit-learn", "scipy"]
|
54
|
-
viz = ["numpy", "pandas", "seaborn"]
|
55
|
-
|
56
|
-
[tool.poetry.dev-dependencies]
|
41
|
+
[project.optional-dependencies]
|
42
|
+
data = [
|
43
|
+
"playwright>=1.49",
|
44
|
+
"pytest-playwright>=0.7"
|
45
|
+
]
|
46
|
+
llm = [
|
47
|
+
"azure-identity>=1.19",
|
48
|
+
"ollama>=0.4",
|
49
|
+
"openai>=1",
|
50
|
+
"python-liquid>=1.12",
|
51
|
+
"tiktoken>=0.8"
|
52
|
+
]
|
53
|
+
statistics = [
|
54
|
+
"numpy>=2.2",
|
55
|
+
"scikit-learn>=1.6",
|
56
|
+
"scipy>=1.15"
|
57
|
+
]
|
58
|
+
viz = [
|
59
|
+
"numpy>=2.2",
|
60
|
+
"pandas>=2.2",
|
61
|
+
"seaborn>=0.13"
|
62
|
+
]
|
63
|
+
|
64
|
+
[tool.poetry.group.dev.dependencies]
|
57
65
|
ipykernel = "*"
|
58
66
|
ipywidgets = "*"
|
59
67
|
|
@@ -87,11 +95,8 @@ mkdocs-literate-nav = "*"
|
|
87
95
|
[tool.poetry.group.typos.dependencies]
|
88
96
|
typos = "*"
|
89
97
|
|
90
|
-
[tool.poetry.scripts]
|
91
|
-
not-again-ai = "not_again_ai.cli:entry_point"
|
92
|
-
|
93
98
|
[build-system]
|
94
|
-
requires = ["poetry-core"]
|
99
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
95
100
|
build-backend = "poetry.core.masonry.api"
|
96
101
|
|
97
102
|
[tool.mypy]
|
@@ -117,6 +122,8 @@ select = [
|
|
117
122
|
"B", # flake8-bugbear
|
118
123
|
"C4", # flake8-comprehensions
|
119
124
|
"ISC", # flake8-implicit-str-concat
|
125
|
+
"PIE", # flake8-pie
|
126
|
+
"PT", # flake-pytest-style
|
120
127
|
"PTH", # flake8-use-pathlib
|
121
128
|
"SIM", # flake8-simplify
|
122
129
|
"TID", # flake8-tidy-imports
|
@@ -141,7 +148,7 @@ filterwarnings = [
|
|
141
148
|
# When running tests, treat warnings as errors (e.g. -Werror).
|
142
149
|
# See: https://docs.pytest.org/en/latest/reference/reference.html#confval-filterwarnings
|
143
150
|
"error",
|
144
|
-
# Add additional warning
|
151
|
+
# Add additional warning suppressions as needed here. For example, if a third-party library
|
145
152
|
# is throwing a deprecation warning that needs to be fixed upstream:
|
146
153
|
# "ignore::DeprecationWarning:typer",
|
147
154
|
"ignore::pytest.PytestUnraisableExceptionWarning"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from not_again_ai.llm.chat_completion.providers.ollama_api import ollama_chat_completion
|
5
|
+
from not_again_ai.llm.chat_completion.providers.openai_api import openai_chat_completion
|
6
|
+
from not_again_ai.llm.chat_completion.types import ChatCompletionRequest, ChatCompletionResponse
|
7
|
+
|
8
|
+
|
9
|
+
def chat_completion(
|
10
|
+
request: ChatCompletionRequest,
|
11
|
+
provider: str,
|
12
|
+
client: Callable[..., Any],
|
13
|
+
) -> ChatCompletionResponse:
|
14
|
+
"""Get a chat completion response from the given provider. Currently supported providers:
|
15
|
+
- `openai` - OpenAI
|
16
|
+
- `azure_openai` - Azure OpenAI
|
17
|
+
- `ollama` - Ollama
|
18
|
+
|
19
|
+
Args:
|
20
|
+
request: Request parameter object
|
21
|
+
provider: The supported provider name
|
22
|
+
client: Client information, see the provider's implementation for what can be provided
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
ChatCompletionResponse: The chat completion response.
|
26
|
+
"""
|
27
|
+
if provider == "openai" or provider == "azure_openai":
|
28
|
+
return openai_chat_completion(request, client)
|
29
|
+
elif provider == "ollama":
|
30
|
+
return ollama_chat_completion(request, client)
|
31
|
+
else:
|
32
|
+
raise ValueError(f"Provider {provider} not supported")
|
@@ -0,0 +1,227 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import time
|
6
|
+
from typing import Any, Literal, cast
|
7
|
+
|
8
|
+
from loguru import logger
|
9
|
+
from ollama import ChatResponse, Client, ResponseError
|
10
|
+
|
11
|
+
from not_again_ai.llm.chat_completion.types import (
|
12
|
+
AssistantMessage,
|
13
|
+
ChatCompletionChoice,
|
14
|
+
ChatCompletionRequest,
|
15
|
+
ChatCompletionResponse,
|
16
|
+
Function,
|
17
|
+
ToolCall,
|
18
|
+
)
|
19
|
+
|
20
|
+
OLLAMA_PARAMETER_MAP = {
|
21
|
+
"frequency_penalty": "repeat_penalty",
|
22
|
+
"max_completion_tokens": "num_predict",
|
23
|
+
"context_window": "num_ctx",
|
24
|
+
"n": None,
|
25
|
+
"tool_choice": None,
|
26
|
+
"reasoning_effort": None,
|
27
|
+
"parallel_tool_calls": None,
|
28
|
+
"logit_bias": None,
|
29
|
+
"top_logprobs": None,
|
30
|
+
"presence_penalty": None,
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
def validate(request: ChatCompletionRequest) -> None:
|
35
|
+
if request.json_mode and request.structured_outputs is not None:
|
36
|
+
raise ValueError("json_schema and json_mode cannot be used together.")
|
37
|
+
|
38
|
+
# Check if any of the parameters set to OLLAMA_PARAMETER_MAP are not None
|
39
|
+
for key, value in OLLAMA_PARAMETER_MAP.items():
|
40
|
+
if value is None and getattr(request, key) is not None:
|
41
|
+
logger.warning(f"Parameter {key} is not supported by Ollama and will be ignored.")
|
42
|
+
|
43
|
+
# If "stop" is not None, check if it is just a string
|
44
|
+
if isinstance(request.stop, list):
|
45
|
+
logger.warning("Parameter 'stop' needs to be a string and not a list. It will be ignored.")
|
46
|
+
request.stop = None
|
47
|
+
|
48
|
+
|
49
|
+
def ollama_chat_completion(
|
50
|
+
request: ChatCompletionRequest,
|
51
|
+
client: Callable[..., Any],
|
52
|
+
) -> ChatCompletionResponse:
|
53
|
+
validate(request)
|
54
|
+
|
55
|
+
kwargs = request.model_dump(mode="json", exclude_none=True)
|
56
|
+
|
57
|
+
# For each key in OLLAMA_PARAMETER_MAP
|
58
|
+
# If it is not None, set the key in kwargs to the value of the corresponding value in OLLAMA_PARAMETER_MAP
|
59
|
+
# If it is None, remove that key from kwargs
|
60
|
+
for key, value in OLLAMA_PARAMETER_MAP.items():
|
61
|
+
if value is not None and key in kwargs:
|
62
|
+
kwargs[value] = kwargs.pop(key)
|
63
|
+
elif value is None and key in kwargs:
|
64
|
+
del kwargs[key]
|
65
|
+
|
66
|
+
# If json_mode is True, set the format to json
|
67
|
+
json_mode = kwargs.get("json_mode", None)
|
68
|
+
if json_mode:
|
69
|
+
kwargs["format"] = "json"
|
70
|
+
kwargs.pop("json_mode")
|
71
|
+
elif json_mode is not None and not json_mode:
|
72
|
+
kwargs.pop("json_mode")
|
73
|
+
|
74
|
+
# If structured_outputs is not None, set the format to structured_outputs
|
75
|
+
if kwargs.get("structured_outputs", None):
|
76
|
+
# Check if the schema is in the OpenAI and pull out the schema
|
77
|
+
if "schema" in kwargs["structured_outputs"]:
|
78
|
+
kwargs["format"] = kwargs["structured_outputs"]["schema"]
|
79
|
+
kwargs.pop("structured_outputs")
|
80
|
+
else:
|
81
|
+
kwargs["format"] = kwargs.pop("structured_outputs")
|
82
|
+
|
83
|
+
option_fields = [
|
84
|
+
"mirostat",
|
85
|
+
"mirostat_eta",
|
86
|
+
"mirostat_tau",
|
87
|
+
"num_ctx",
|
88
|
+
"repeat_last_n",
|
89
|
+
"repeat_penalty",
|
90
|
+
"temperature",
|
91
|
+
"seed",
|
92
|
+
"stop",
|
93
|
+
"tfs_z",
|
94
|
+
"num_predict",
|
95
|
+
"top_k",
|
96
|
+
"top_p",
|
97
|
+
"min_p",
|
98
|
+
]
|
99
|
+
# For each field in option_fields, if it is in kwargs, make it under an options dictionary
|
100
|
+
options = {}
|
101
|
+
for field in option_fields:
|
102
|
+
if field in kwargs:
|
103
|
+
options[field] = kwargs.pop(field)
|
104
|
+
kwargs["options"] = options
|
105
|
+
|
106
|
+
for message in kwargs["messages"]:
|
107
|
+
role = message.get("role", None)
|
108
|
+
# For each ToolMessage, remove the name field
|
109
|
+
if role is not None and role == "tool":
|
110
|
+
message.pop("name")
|
111
|
+
|
112
|
+
# For each AssistantMessage with tool calls, remove the id field
|
113
|
+
if role is not None and role == "assistant" and message.get("tool_calls", None):
|
114
|
+
for tool_call in message["tool_calls"]:
|
115
|
+
tool_call.pop("id")
|
116
|
+
|
117
|
+
# Content and images need to be separated
|
118
|
+
images = []
|
119
|
+
content = ""
|
120
|
+
if isinstance(message["content"], list):
|
121
|
+
for item in message["content"]:
|
122
|
+
if item["type"] == "image_url":
|
123
|
+
image_url = item["image_url"]["url"]
|
124
|
+
# Remove the data URL prefix if present
|
125
|
+
if image_url.startswith("data:"):
|
126
|
+
image_url = image_url.split("base64,", 1)[1]
|
127
|
+
images.append(image_url)
|
128
|
+
else:
|
129
|
+
content += item["text"]
|
130
|
+
else:
|
131
|
+
content = message["content"]
|
132
|
+
|
133
|
+
message["content"] = content
|
134
|
+
if len(images) > 1:
|
135
|
+
images = images[:1]
|
136
|
+
logger.warning("Ollama model only supports a single image per message. Using only the first images.")
|
137
|
+
message["images"] = images
|
138
|
+
|
139
|
+
try:
|
140
|
+
start_time = time.time()
|
141
|
+
response: ChatResponse = client(**kwargs)
|
142
|
+
end_time = time.time()
|
143
|
+
response_duration = round(end_time - start_time, 4)
|
144
|
+
except ResponseError as e:
|
145
|
+
# If the error says "model 'model' not found" use regex then raise a more specific error
|
146
|
+
expected_pattern = f"model '{request.model}' not found"
|
147
|
+
if re.search(expected_pattern, e.error):
|
148
|
+
raise ResponseError(f"Model '{request.model}' not found.") from e
|
149
|
+
else:
|
150
|
+
raise ResponseError(e.error) from e
|
151
|
+
|
152
|
+
errors = ""
|
153
|
+
|
154
|
+
# Handle tool calls
|
155
|
+
tool_calls: list[ToolCall] | None = None
|
156
|
+
if response.message.tool_calls:
|
157
|
+
parsed_tool_calls: list[ToolCall] = []
|
158
|
+
for tool_call in response.message.tool_calls:
|
159
|
+
tool_name = tool_call.function.name
|
160
|
+
if request.tools and tool_name not in [tool["function"]["name"] for tool in request.tools]:
|
161
|
+
errors += f"Tool call {tool_call} has an invalid tool name: {tool_name}\n"
|
162
|
+
tool_args = tool_call.function.arguments
|
163
|
+
parsed_tool_calls.append(
|
164
|
+
ToolCall(
|
165
|
+
id="",
|
166
|
+
function=Function(
|
167
|
+
name=tool_name,
|
168
|
+
arguments=tool_args,
|
169
|
+
),
|
170
|
+
)
|
171
|
+
)
|
172
|
+
tool_calls = parsed_tool_calls
|
173
|
+
|
174
|
+
json_message = None
|
175
|
+
if (request.json_mode or (request.structured_outputs is not None)) and response.message.content:
|
176
|
+
try:
|
177
|
+
json_message = json.loads(response.message.content)
|
178
|
+
except json.JSONDecodeError:
|
179
|
+
errors += "Message failed to parse into JSON\n"
|
180
|
+
|
181
|
+
finish_reason = cast(
|
182
|
+
Literal["stop", "length", "tool_calls", "content_filter"],
|
183
|
+
"stop" if response.done_reason is None else response.done_reason or "stop",
|
184
|
+
)
|
185
|
+
|
186
|
+
choice = ChatCompletionChoice(
|
187
|
+
message=AssistantMessage(
|
188
|
+
content=response.message.content or "",
|
189
|
+
tool_calls=tool_calls,
|
190
|
+
),
|
191
|
+
finish_reason=finish_reason,
|
192
|
+
json_message=json_message,
|
193
|
+
)
|
194
|
+
|
195
|
+
return ChatCompletionResponse(
|
196
|
+
choices=[choice],
|
197
|
+
errors=errors.strip(),
|
198
|
+
completion_tokens=response.get("eval_count", -1),
|
199
|
+
prompt_tokens=response.get("prompt_eval_count", -1),
|
200
|
+
response_duration=response_duration,
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
def ollama_client(host: str | None = None, timeout: float | None = None) -> Callable[..., Any]:
|
205
|
+
"""Create an Ollama client instance based on the specified host or will read from the OLLAMA_HOST environment variable.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
host (str, optional): The host URL of the Ollama server.
|
209
|
+
timeout (float, optional): The timeout for requests
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
Client: An instance of the Ollama client.
|
213
|
+
|
214
|
+
Examples:
|
215
|
+
>>> client = client(host="http://localhost:11434")
|
216
|
+
"""
|
217
|
+
if host is None:
|
218
|
+
host = os.getenv("OLLAMA_HOST")
|
219
|
+
if host is None:
|
220
|
+
logger.warning("OLLAMA_HOST environment variable not set, using default host: http://localhost:11434")
|
221
|
+
host = "http://localhost:11434"
|
222
|
+
|
223
|
+
def client_callable(**kwargs: Any) -> Any:
|
224
|
+
client = Client(host=host, timeout=timeout)
|
225
|
+
return client.chat(**kwargs)
|
226
|
+
|
227
|
+
return client_callable
|