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.
Files changed (58) hide show
  1. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/PKG-INFO +24 -40
  2. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/README.md +4 -18
  3. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/pyproject.toml +49 -42
  4. not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/__init__.py +4 -0
  5. not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/interface.py +32 -0
  6. not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/providers/ollama_api.py +227 -0
  7. not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/providers/openai_api.py +290 -0
  8. not_again_ai-0.16.0/src/not_again_ai/llm/chat_completion/types.py +145 -0
  9. not_again_ai-0.16.0/src/not_again_ai/llm/embedding/__init__.py +4 -0
  10. not_again_ai-0.16.0/src/not_again_ai/llm/embedding/interface.py +28 -0
  11. not_again_ai-0.16.0/src/not_again_ai/llm/embedding/providers/ollama_api.py +87 -0
  12. not_again_ai-0.16.0/src/not_again_ai/llm/embedding/providers/openai_api.py +126 -0
  13. not_again_ai-0.16.0/src/not_again_ai/llm/embedding/types.py +23 -0
  14. not_again_ai-0.16.0/src/not_again_ai/llm/prompting/__init__.py +3 -0
  15. not_again_ai-0.16.0/src/not_again_ai/llm/prompting/compile_prompt.py +125 -0
  16. not_again_ai-0.16.0/src/not_again_ai/llm/prompting/interface.py +46 -0
  17. not_again_ai-0.16.0/src/not_again_ai/llm/prompting/providers/openai_tiktoken.py +122 -0
  18. not_again_ai-0.16.0/src/not_again_ai/llm/prompting/types.py +43 -0
  19. not_again_ai-0.14.0/src/not_again_ai/llm/gh_models/azure_ai_client.py +0 -20
  20. not_again_ai-0.14.0/src/not_again_ai/llm/gh_models/chat_completion.py +0 -81
  21. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/chat_completion.py +0 -339
  22. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/context_management.py +0 -70
  23. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/embeddings.py +0 -62
  24. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/openai_client.py +0 -78
  25. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/prompts.py +0 -191
  26. not_again_ai-0.14.0/src/not_again_ai/llm/openai_api/tokens.py +0 -184
  27. not_again_ai-0.14.0/src/not_again_ai/local_llm/__init__.py +0 -27
  28. not_again_ai-0.14.0/src/not_again_ai/local_llm/chat_completion.py +0 -105
  29. not_again_ai-0.14.0/src/not_again_ai/local_llm/huggingface/chat_completion.py +0 -59
  30. not_again_ai-0.14.0/src/not_again_ai/local_llm/huggingface/helpers.py +0 -23
  31. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/__init__.py +0 -0
  32. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/chat_completion.py +0 -111
  33. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/model_mapping.py +0 -17
  34. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/ollama_client.py +0 -24
  35. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/service.py +0 -81
  36. not_again_ai-0.14.0/src/not_again_ai/local_llm/ollama/tokens.py +0 -104
  37. not_again_ai-0.14.0/src/not_again_ai/local_llm/prompts.py +0 -38
  38. not_again_ai-0.14.0/src/not_again_ai/local_llm/tokens.py +0 -90
  39. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/LICENSE +0 -0
  40. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/__init__.py +0 -0
  41. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/__init__.py +0 -0
  42. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/file_system.py +0 -0
  43. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/base/parallel.py +0 -0
  44. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/data/__init__.py +0 -0
  45. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/data/web.py +0 -0
  46. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/llm/__init__.py +0 -0
  47. {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
  48. {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
  49. {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
  50. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/py.typed +0 -0
  51. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/statistics/__init__.py +0 -0
  52. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/statistics/dependence.py +0 -0
  53. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/__init__.py +0 -0
  54. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/barplots.py +0 -0
  55. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/distributions.py +0 -0
  56. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/scatterplot.py +0 -0
  57. {not_again_ai-0.14.0 → not_again_ai-0.16.0}/src/not_again_ai/viz/time_series.py +0 -0
  58. {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
1
+ Metadata-Version: 2.3
2
2
  Name: not-again-ai
3
- Version: 0.14.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,<4.0
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-ai-inference (==1.0.0b5) ; extra == "llm"
26
- Requires-Dist: azure-identity (>=1.19,<2.0) ; extra == "llm"
27
- Requires-Dist: jinja2 (>=3.1,<4.0) ; extra == "local-llm"
28
- Requires-Dist: loguru (>=0.7,<0.8)
29
- Requires-Dist: numpy (>=2.1,<3.0) ; extra == "statistics" or extra == "viz"
30
- Requires-Dist: ollama (>=0.3,<0.4) ; extra == "local-llm"
31
- Requires-Dist: openai (>=1.52,<2.0) ; extra == "llm"
32
- Requires-Dist: pandas (>=2.2,<3.0) ; extra == "viz"
33
- Requires-Dist: pydantic (>=2.9,<3.0)
34
- Requires-Dist: pytest-playwright (>=0.5,<0.6) ; extra == "data"
35
- Requires-Dist: python-liquid (>=1.12,<2.0) ; extra == "llm"
36
- Requires-Dist: scikit-learn (>=1.5,<2.0) ; extra == "statistics"
37
- Requires-Dist: scipy (>=1.14,<2.0) ; extra == "statistics"
38
- Requires-Dist: seaborn (>=0.13,<0.14) ; extra == "viz"
39
- Requires-Dist: tiktoken (>=0.8,<0.9) ; extra == "llm"
40
- Requires-Dist: transformers (>=4.45,<5.0) ; extra == "local-llm"
41
- Project-URL: Documentation, https://github.com/DaveCoDev/not-again-ai
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,local_llm,statistics,viz]
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. Setup GitHub Models
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 install --all-extras
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 install`
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,local_llm,statistics,viz]
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. Setup GitHub Models
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 install --all-extras
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 install`
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
- [tool.poetry]
1
+ [project]
2
2
  name = "not-again-ai"
3
- version = "0.14.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 = ["DaveCoDev <dave.co.dev@gmail.com>"]
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
- [tool.poetry.dependencies]
24
- # Some packages, such as scipy, constrain their upper bound of Python versions they support.
25
- # Without also constraining the upper bound here, Poetry will not select those versions and will
26
- # result in an old version being resolved/locked.
27
- python = "^3.11 || ^3.12"
28
-
29
- loguru = { version = "^0.7" }
30
- pydantic = { version = "^2.9"}
31
-
32
-
33
- # Optional dependencies are defined here, and groupings are defined below.
34
- azure-ai-inference = { version = "==1.0.0b5", optional = true }
35
- azure-identity = { version = "^1.19", optional = true }
36
- jinja2 = { version = "^3.1", optional = true }
37
- numpy = { version = "^2.1", optional = true }
38
- ollama = { version = "^0.3", optional = true }
39
- openai = { version = "^1.52", optional = true }
40
- pandas = { version = "^2.2", optional = true }
41
- pytest-playwright = { version = "^0.5", optional = true }
42
- python-liquid = { version = "^1.12", optional = true }
43
- scipy = { version = "^1.14", optional = true }
44
- scikit-learn = { version = "^1.5", optional = true }
45
- seaborn = { version = "^0.13", optional = true }
46
- tiktoken = { version = "^0.8", optional = true }
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 supressions as needed here. For example, if a third-party library
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,4 @@
1
+ from not_again_ai.llm.chat_completion.interface import chat_completion
2
+ from not_again_ai.llm.chat_completion.types import ChatCompletionRequest
3
+
4
+ __all__ = ["ChatCompletionRequest", "chat_completion"]
@@ -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