not-again-ai 0.15.0__py3-none-any.whl → 0.16.0__py3-none-any.whl

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,4 @@
1
+ from not_again_ai.llm.embedding.interface import create_embeddings
2
+ from not_again_ai.llm.embedding.types import EmbeddingRequest
3
+
4
+ __all__ = ["EmbeddingRequest", "create_embeddings"]
@@ -0,0 +1,28 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from not_again_ai.llm.embedding.providers.ollama_api import ollama_create_embeddings
5
+ from not_again_ai.llm.embedding.providers.openai_api import openai_create_embeddings
6
+ from not_again_ai.llm.embedding.types import EmbeddingRequest, EmbeddingResponse
7
+
8
+
9
+ def create_embeddings(request: EmbeddingRequest, provider: str, client: Callable[..., Any]) -> EmbeddingResponse:
10
+ """Get a embedding response from the given provider. Currently supported providers:
11
+ - `openai` - OpenAI
12
+ - `azure_openai` - Azure OpenAI
13
+ - `ollama` - Ollama
14
+
15
+ Args:
16
+ request: Request parameter object
17
+ provider: The supported provider name
18
+ client: Client information, see the provider's implementation for what can be provided
19
+
20
+ Returns:
21
+ EmbeddingResponse: The embedding response.
22
+ """
23
+ if provider == "openai" or provider == "azure_openai":
24
+ return openai_create_embeddings(request, client)
25
+ elif provider == "ollama":
26
+ return ollama_create_embeddings(request, client)
27
+ else:
28
+ raise ValueError(f"Provider {provider} not supported")
File without changes
@@ -0,0 +1,87 @@
1
+ from collections.abc import Callable
2
+ import os
3
+ import re
4
+ import time
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+ from ollama import Client, EmbedResponse, ResponseError
9
+
10
+ from not_again_ai.llm.embedding.types import EmbeddingObject, EmbeddingRequest, EmbeddingResponse
11
+
12
+ OLLAMA_PARAMETER_MAP = {
13
+ "dimensions": None,
14
+ }
15
+
16
+
17
+ def validate(request: EmbeddingRequest) -> None:
18
+ # Check if any of the parameters set to OLLAMA_PARAMETER_MAP are not None
19
+ for key, value in OLLAMA_PARAMETER_MAP.items():
20
+ if value is None and getattr(request, key) is not None:
21
+ logger.warning(f"Parameter {key} is not supported by Ollama and will be ignored.")
22
+
23
+
24
+ def ollama_create_embeddings(request: EmbeddingRequest, client: Callable[..., Any]) -> EmbeddingResponse:
25
+ validate(request)
26
+ kwargs = request.model_dump(mode="json", exclude_none=True)
27
+
28
+ # For each key in OLLAMA_PARAMETER_MAP
29
+ # If it is not None, set the key in kwargs to the value of the corresponding value in OLLAMA_PARAMETER_MAP
30
+ # If it is None, remove that key from kwargs
31
+ for key, value in OLLAMA_PARAMETER_MAP.items():
32
+ if value is not None and key in kwargs:
33
+ kwargs[value] = kwargs.pop(key)
34
+ elif value is None and key in kwargs:
35
+ del kwargs[key]
36
+
37
+ # Explicitly set truncate to True (it is the default)
38
+ kwargs["truncate"] = True
39
+
40
+ try:
41
+ start_time = time.time()
42
+ response: EmbedResponse = client(**kwargs)
43
+ end_time = time.time()
44
+ response_duration = round(end_time - start_time, 4)
45
+ except ResponseError as e:
46
+ # If the error says "model 'model' not found" use regex then raise a more specific error
47
+ expected_pattern = f"model '{request.model}' not found"
48
+ if re.search(expected_pattern, e.error):
49
+ raise ResponseError(f"Model '{request.model}' not found.") from e
50
+ else:
51
+ raise ResponseError(e.error) from e
52
+
53
+ embeddings: list[EmbeddingObject] = []
54
+ for index, embedding in enumerate(response.embeddings):
55
+ embeddings.append(EmbeddingObject(embedding=list(embedding), index=index))
56
+
57
+ return EmbeddingResponse(
58
+ embeddings=embeddings,
59
+ response_duration=response_duration,
60
+ total_tokens=response.prompt_eval_count,
61
+ )
62
+
63
+
64
+ def ollama_client(host: str | None = None, timeout: float | None = None) -> Callable[..., Any]:
65
+ """Create an Ollama client instance based on the specified host or will read from the OLLAMA_HOST environment variable.
66
+
67
+ Args:
68
+ host (str, optional): The host URL of the Ollama server.
69
+ timeout (float, optional): The timeout for requests
70
+
71
+ Returns:
72
+ Client: An instance of the Ollama client.
73
+
74
+ Examples:
75
+ >>> client = client(host="http://localhost:11434")
76
+ """
77
+ if host is None:
78
+ host = os.getenv("OLLAMA_HOST")
79
+ if host is None:
80
+ logger.warning("OLLAMA_HOST environment variable not set, using default host: http://localhost:11434")
81
+ host = "http://localhost:11434"
82
+
83
+ def client_callable(**kwargs: Any) -> Any:
84
+ client = Client(host=host, timeout=timeout)
85
+ return client.embed(**kwargs)
86
+
87
+ return client_callable
@@ -0,0 +1,126 @@
1
+ from collections.abc import Callable
2
+ import time
3
+ from typing import Any, Literal
4
+
5
+ from azure.identity import DefaultAzureCredential, get_bearer_token_provider
6
+ from openai import AzureOpenAI, OpenAI
7
+
8
+ from not_again_ai.llm.embedding.types import EmbeddingObject, EmbeddingRequest, EmbeddingResponse
9
+
10
+
11
+ def openai_create_embeddings(request: EmbeddingRequest, client: Callable[..., Any]) -> EmbeddingResponse:
12
+ kwargs = request.model_dump(mode="json", exclude_none=True)
13
+
14
+ start_time = time.time()
15
+ response = client(**kwargs)
16
+ end_time = time.time()
17
+ response_duration = round(end_time - start_time, 4)
18
+
19
+ embeddings: list[EmbeddingObject] = []
20
+ for data in response["data"]:
21
+ embeddings.append(EmbeddingObject(embedding=data["embedding"], index=data["index"]))
22
+
23
+ return EmbeddingResponse(
24
+ embeddings=embeddings,
25
+ response_duration=response_duration,
26
+ total_tokens=response["usage"]["total_tokens"],
27
+ )
28
+
29
+
30
+ def create_client_callable(client_class: type[OpenAI | AzureOpenAI], **client_args: Any) -> Callable[..., Any]:
31
+ """Creates a callable that instantiates and uses an OpenAI client.
32
+
33
+ Args:
34
+ client_class: The OpenAI client class to instantiate (OpenAI or AzureOpenAI)
35
+ **client_args: Arguments to pass to the client constructor
36
+
37
+ Returns:
38
+ A callable that creates a client and returns completion results
39
+ """
40
+ filtered_args = {k: v for k, v in client_args.items() if v is not None}
41
+
42
+ def client_callable(**kwargs: Any) -> Any:
43
+ client = client_class(**filtered_args)
44
+ completion = client.embeddings.create(**kwargs)
45
+ return completion.to_dict()
46
+
47
+ return client_callable
48
+
49
+
50
+ class InvalidOAIAPITypeError(Exception):
51
+ """Raised when an invalid OAIAPIType string is provided."""
52
+
53
+
54
+ def openai_client(
55
+ api_type: Literal["openai", "azure_openai"] = "openai",
56
+ api_key: str | None = None,
57
+ organization: str | None = None,
58
+ aoai_api_version: str = "2024-06-01",
59
+ azure_endpoint: str | None = None,
60
+ timeout: float | None = None,
61
+ max_retries: int | None = None,
62
+ ) -> Callable[..., Any]:
63
+ """Create an OpenAI or Azure OpenAI client instance based on the specified API type and other provided parameters.
64
+
65
+ It is preferred to use RBAC authentication for Azure OpenAI. You must be signed in with the Azure CLI and have correct role assigned.
66
+ See https://techcommunity.microsoft.com/t5/microsoft-developer-community/using-keyless-authentication-with-azure-openai/ba-p/4111521
67
+
68
+ Args:
69
+ api_type (str, optional): Type of the API to be used. Accepted values are 'openai' or 'azure_openai'.
70
+ Defaults to 'openai'.
71
+ api_key (str, optional): The API key to authenticate the client. If not provided,
72
+ OpenAI automatically uses `OPENAI_API_KEY` from the environment.
73
+ If provided for Azure OpenAI, it will be used for authentication instead of the Azure AD token provider.
74
+ organization (str, optional): The ID of the organization. If not provided,
75
+ OpenAI automotically uses `OPENAI_ORG_ID` from the environment.
76
+ aoai_api_version (str, optional): Only applicable if using Azure OpenAI https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning
77
+ azure_endpoint (str, optional): The endpoint to use for Azure OpenAI.
78
+ timeout (float, optional): By default requests time out after 10 minutes.
79
+ max_retries (int, optional): Certain errors are automatically retried 2 times by default,
80
+ with a short exponential backoff. Connection errors (for example, due to a network connectivity problem),
81
+ 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default.
82
+
83
+ Returns:
84
+ Callable[..., Any]: A callable that creates a client and returns completion results
85
+
86
+
87
+ Raises:
88
+ InvalidOAIAPITypeError: If an invalid API type string is provided.
89
+ NotImplementedError: If the specified API type is recognized but not yet supported (e.g., 'azure_openai').
90
+ """
91
+ if api_type not in ["openai", "azure_openai"]:
92
+ raise InvalidOAIAPITypeError(f"Invalid OAIAPIType: {api_type}. Must be 'openai' or 'azure_openai'.")
93
+
94
+ if api_type == "openai":
95
+ return create_client_callable(
96
+ OpenAI,
97
+ api_key=api_key,
98
+ organization=organization,
99
+ timeout=timeout,
100
+ max_retries=max_retries,
101
+ )
102
+ elif api_type == "azure_openai":
103
+ if api_key:
104
+ return create_client_callable(
105
+ AzureOpenAI,
106
+ api_version=aoai_api_version,
107
+ azure_endpoint=azure_endpoint,
108
+ api_key=api_key,
109
+ timeout=timeout,
110
+ max_retries=max_retries,
111
+ )
112
+ else:
113
+ azure_credential = DefaultAzureCredential()
114
+ ad_token_provider = get_bearer_token_provider(
115
+ azure_credential, "https://cognitiveservices.azure.com/.default"
116
+ )
117
+ return create_client_callable(
118
+ AzureOpenAI,
119
+ api_version=aoai_api_version,
120
+ azure_endpoint=azure_endpoint,
121
+ azure_ad_token_provider=ad_token_provider,
122
+ timeout=timeout,
123
+ max_retries=max_retries,
124
+ )
125
+ else:
126
+ raise NotImplementedError(f"API type '{api_type}' is invalid.")
@@ -0,0 +1,23 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class EmbeddingRequest(BaseModel):
7
+ input: str | list[str]
8
+ model: str
9
+ dimensions: int | None = Field(default=None)
10
+
11
+
12
+ class EmbeddingObject(BaseModel):
13
+ embedding: list[float]
14
+ index: int
15
+
16
+
17
+ class EmbeddingResponse(BaseModel):
18
+ embeddings: list[EmbeddingObject]
19
+ total_tokens: int | None = Field(default=None)
20
+ response_duration: float
21
+
22
+ errors: str = Field(default="")
23
+ extras: Any | None = Field(default=None)
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ from collections.abc import Sequence
2
3
  from copy import deepcopy
3
4
  import mimetypes
4
5
  from pathlib import Path
@@ -8,10 +9,27 @@ from liquid import Template
8
9
  from openai.lib._pydantic import to_strict_json_schema
9
10
  from pydantic import BaseModel
10
11
 
11
- from not_again_ai.llm.chat_completion.types import MessageT, TextContent
12
+ from not_again_ai.llm.chat_completion.types import MessageT
12
13
 
13
14
 
14
- def compile_messages(messages: list[MessageT], variables: dict[str, str]) -> list[MessageT]:
15
+ def _apply_templates(value: Any, variables: dict[str, str]) -> Any:
16
+ """Recursively applies Liquid templating to all string fields within the given value."""
17
+ if isinstance(value, str):
18
+ return Template(value).render(**variables)
19
+ elif isinstance(value, list):
20
+ return [_apply_templates(item, variables) for item in value]
21
+ elif isinstance(value, dict):
22
+ return {key: _apply_templates(val, variables) for key, val in value.items()}
23
+ elif isinstance(value, BaseModel):
24
+ # Process each field in the BaseModel by converting it to a dict,
25
+ # applying templating to its values, and then re-instantiating the model.
26
+ processed_data = {key: _apply_templates(val, variables) for key, val in value.model_dump().items()}
27
+ return value.__class__(**processed_data)
28
+ else:
29
+ return value
30
+
31
+
32
+ def compile_messages(messages: Sequence[MessageT], variables: dict[str, str]) -> Sequence[MessageT]:
15
33
  """Compiles messages using Liquid templating and the provided variables.
16
34
  Calls Template(content_part).render(**variables) on each text content part.
17
35
 
@@ -23,19 +41,28 @@ def compile_messages(messages: list[MessageT], variables: dict[str, str]) -> lis
23
41
  The same list of messages with the content parts injected with the variables.
24
42
  """
25
43
  messages_formatted = deepcopy(messages)
26
- for message in messages_formatted:
27
- if isinstance(message.content, str):
28
- # For simple string content, apply template directly
29
- message.content = Template(message.content).render(**variables)
30
- elif isinstance(message.content, list):
31
- # For UserMessage with content parts
32
- for content_part in message.content:
33
- if isinstance(content_part, TextContent):
34
- content_part.text = Template(content_part.text).render(**variables)
35
- # ImageContent parts are left unchanged
44
+ messages_formatted = [_apply_templates(message, variables) for message in messages_formatted]
36
45
  return messages_formatted
37
46
 
38
47
 
48
+ def compile_tools(tools: Sequence[dict[str, Any]], variables: dict[str, str]) -> Sequence[dict[str, Any]]:
49
+ """Compiles a list of tool argument dictionaries using Liquid templating and provided variables.
50
+
51
+ Each dictionary in the list is deep copied and processed recursively to substitute any Liquid
52
+ templates present in its data structure.
53
+
54
+ Args:
55
+ tools: A list of dictionaries representing tool arguments, where values can include Liquid templates.
56
+ variables: A dictionary of variables to substitute into the Liquid templates.
57
+
58
+ Returns:
59
+ A new list of dictionaries with the Liquid templates replaced by their corresponding variable values.
60
+ """
61
+ tools_formatted = deepcopy(tools)
62
+ tools_formatted = [_apply_templates(tool, variables) for tool in tools_formatted]
63
+ return tools_formatted
64
+
65
+
39
66
  def encode_image(image_path: Path) -> str:
40
67
  """Encodes an image file at the given Path to base64.
41
68
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: not-again-ai
3
- Version: 0.15.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
5
  License: MIT
6
6
  Author: DaveCoDev
@@ -25,11 +25,11 @@ Requires-Dist: loguru (>=0.7)
25
25
  Requires-Dist: numpy (>=2.2) ; extra == "statistics"
26
26
  Requires-Dist: numpy (>=2.2) ; extra == "viz"
27
27
  Requires-Dist: ollama (>=0.4) ; extra == "llm"
28
- Requires-Dist: openai (>=1.60) ; extra == "llm"
28
+ Requires-Dist: openai (>=1) ; extra == "llm"
29
29
  Requires-Dist: pandas (>=2.2) ; extra == "viz"
30
30
  Requires-Dist: playwright (>=1.49) ; extra == "data"
31
31
  Requires-Dist: pydantic (>=2.10)
32
- Requires-Dist: pytest-playwright (>=0.6) ; extra == "data"
32
+ Requires-Dist: pytest-playwright (>=0.7) ; extra == "data"
33
33
  Requires-Dist: python-liquid (>=1.12) ; extra == "llm"
34
34
  Requires-Dist: scikit-learn (>=1.6) ; extra == "statistics"
35
35
  Requires-Dist: scipy (>=1.15) ; extra == "statistics"
@@ -11,8 +11,14 @@ not_again_ai/llm/chat_completion/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW
11
11
  not_again_ai/llm/chat_completion/providers/ollama_api.py,sha256=iBTMyF8edo8uxxrorNPtShzmCXG7m0RlEBunWLSO4Mo,7999
12
12
  not_again_ai/llm/chat_completion/providers/openai_api.py,sha256=S7TZhDIQ_xpp3JakRVcd3Gpw2UjeHCETdA9MfRKUjCU,12294
13
13
  not_again_ai/llm/chat_completion/types.py,sha256=q8APUWWzwCKL0Rs_zEFfph9uBcwh5nAT0f0rp4crvk0,4039
14
+ not_again_ai/llm/embedding/__init__.py,sha256=wscUfROukvw0M0vYccfaVTdXV0P-eICAT5mqM0LaHHc,182
15
+ not_again_ai/llm/embedding/interface.py,sha256=Hj3UiktXEeCUeMwpIDtRkwBfKgaJSnJvclLNyjwUAtE,1144
16
+ not_again_ai/llm/embedding/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ not_again_ai/llm/embedding/providers/ollama_api.py,sha256=m-OCis9WAUT2baGsGVPzejlive40eSNyO6tHmPh6joM,3201
18
+ not_again_ai/llm/embedding/providers/openai_api.py,sha256=JFFqbq0O5snIEnr9VESdp5xehikQBPbs7nwyE6acFsY,5441
19
+ not_again_ai/llm/embedding/types.py,sha256=J4FFLx35Aow2kOaafDReeY9cUNqhWMjaAk5gXkX7SVk,506
14
20
  not_again_ai/llm/prompting/__init__.py,sha256=7YnHro1yH01FLGnao27WyqQDFjNYf9npE5UxoR9YrUU,84
15
- not_again_ai/llm/prompting/compile_messages.py,sha256=HmVCQ-0iVg8vFWZyppxUf9m_ae5c8rK1Zx8ySPD1Bg8,3452
21
+ not_again_ai/llm/prompting/compile_prompt.py,sha256=lnbTOoTc7PumyP_GhfHaLZHp3UUpSB7VAeWOilS1wpI,4703
16
22
  not_again_ai/llm/prompting/interface.py,sha256=SMKYabmu3zTWbEDukU6aLU_JQ88apeBWWOF_qZ0s3ww,1783
17
23
  not_again_ai/llm/prompting/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
24
  not_again_ai/llm/prompting/providers/openai_tiktoken.py,sha256=8YrEiK3ZHyKVGiVsJ_Rd6eVdISIvcub7ooj-HB7Prsc,4536
@@ -26,7 +32,7 @@ not_again_ai/viz/distributions.py,sha256=OyWwJaNI6lMRm_iSrhq-CORLNvXfeuLSgDtVo3u
26
32
  not_again_ai/viz/scatterplot.py,sha256=5CUOWeknbBOaZPeX9oPin5sBkRKEwk8qeFH45R-9LlY,2292
27
33
  not_again_ai/viz/time_series.py,sha256=pOGZqXp_2nd6nKo-PUQNCtmMh__69jxQ6bQibTGLwZA,5212
28
34
  not_again_ai/viz/utils.py,sha256=hN7gwxtBt3U6jQni2K8j5m5pCXpaJDoNzGhBBikEU28,238
29
- not_again_ai-0.15.0.dist-info/LICENSE,sha256=btjOgNGpp-ux5xOo1Gx1MddxeWtT9sof3s3Nui29QfA,1071
30
- not_again_ai-0.15.0.dist-info/METADATA,sha256=_vGJUluFVmoYQrNwLGMh5NWtH6aiJ5BG8G8hlZ5TRpE,15038
31
- not_again_ai-0.15.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
32
- not_again_ai-0.15.0.dist-info/RECORD,,
35
+ not_again_ai-0.16.0.dist-info/LICENSE,sha256=btjOgNGpp-ux5xOo1Gx1MddxeWtT9sof3s3Nui29QfA,1071
36
+ not_again_ai-0.16.0.dist-info/METADATA,sha256=kvwxTcEi-elRl-LuHyh2QtFLrpYHd-U6HjyuAkHYvWQ,15035
37
+ not_again_ai-0.16.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
38
+ not_again_ai-0.16.0.dist-info/RECORD,,