not-again-ai 0.16.1__py3-none-any.whl → 0.18.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.
- not_again_ai/llm/__init__.py +0 -15
- not_again_ai/llm/chat_completion/__init__.py +2 -2
- not_again_ai/llm/chat_completion/interface.py +37 -4
- not_again_ai/llm/chat_completion/providers/anthropic_api.py +180 -0
- not_again_ai/llm/chat_completion/providers/ollama_api.py +80 -12
- not_again_ai/llm/chat_completion/providers/openai_api.py +180 -38
- not_again_ai/llm/chat_completion/types.py +49 -1
- not_again_ai/llm/prompting/compile_prompt.py +3 -3
- {not_again_ai-0.16.1.dist-info → not_again_ai-0.18.0.dist-info}/METADATA +66 -164
- {not_again_ai-0.16.1.dist-info → not_again_ai-0.18.0.dist-info}/RECORD +15 -14
- {not_again_ai-0.16.1.dist-info → not_again_ai-0.18.0.dist-info}/WHEEL +1 -1
- {not_again_ai-0.16.1.dist-info → not_again_ai-0.18.0.dist-info/licenses}/LICENSE +0 -0
not_again_ai/llm/__init__.py
CHANGED
@@ -1,15 +0,0 @@
|
|
1
|
-
import importlib.util
|
2
|
-
|
3
|
-
if (
|
4
|
-
importlib.util.find_spec("liquid") is None
|
5
|
-
or importlib.util.find_spec("openai") is None
|
6
|
-
or importlib.util.find_spec("tiktoken") is None
|
7
|
-
):
|
8
|
-
raise ImportError(
|
9
|
-
"not_again_ai.llm requires the 'llm' extra to be installed. "
|
10
|
-
"You can install it using 'pip install not_again_ai[llm]'."
|
11
|
-
)
|
12
|
-
else:
|
13
|
-
import liquid # noqa: F401
|
14
|
-
import openai # noqa: F401
|
15
|
-
import tiktoken # noqa: F401
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from not_again_ai.llm.chat_completion.interface import chat_completion
|
1
|
+
from not_again_ai.llm.chat_completion.interface import chat_completion, chat_completion_stream
|
2
2
|
from not_again_ai.llm.chat_completion.types import ChatCompletionRequest
|
3
3
|
|
4
|
-
__all__ = ["ChatCompletionRequest", "chat_completion"]
|
4
|
+
__all__ = ["ChatCompletionRequest", "chat_completion", "chat_completion_stream"]
|
@@ -1,9 +1,10 @@
|
|
1
|
-
from collections.abc import Callable
|
1
|
+
from collections.abc import AsyncGenerator, Callable
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from not_again_ai.llm.chat_completion.providers.
|
5
|
-
from not_again_ai.llm.chat_completion.providers.
|
6
|
-
from not_again_ai.llm.chat_completion.
|
4
|
+
from not_again_ai.llm.chat_completion.providers.anthropic_api import anthropic_chat_completion
|
5
|
+
from not_again_ai.llm.chat_completion.providers.ollama_api import ollama_chat_completion, ollama_chat_completion_stream
|
6
|
+
from not_again_ai.llm.chat_completion.providers.openai_api import openai_chat_completion, openai_chat_completion_stream
|
7
|
+
from not_again_ai.llm.chat_completion.types import ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse
|
7
8
|
|
8
9
|
|
9
10
|
def chat_completion(
|
@@ -28,5 +29,37 @@ def chat_completion(
|
|
28
29
|
return openai_chat_completion(request, client)
|
29
30
|
elif provider == "ollama":
|
30
31
|
return ollama_chat_completion(request, client)
|
32
|
+
elif provider == "anthropic":
|
33
|
+
return anthropic_chat_completion(request, client)
|
34
|
+
else:
|
35
|
+
raise ValueError(f"Provider {provider} not supported")
|
36
|
+
|
37
|
+
|
38
|
+
async def chat_completion_stream(
|
39
|
+
request: ChatCompletionRequest,
|
40
|
+
provider: str,
|
41
|
+
client: Callable[..., Any],
|
42
|
+
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
43
|
+
"""Stream a chat completion response from the given provider. Currently supported providers:
|
44
|
+
- `openai` - OpenAI
|
45
|
+
- `azure_openai` - Azure OpenAI
|
46
|
+
- `ollama` - Ollama
|
47
|
+
- `anthropic` - Anthropic
|
48
|
+
|
49
|
+
Args:
|
50
|
+
request: Request parameter object
|
51
|
+
provider: The supported provider name
|
52
|
+
client: Client information, see the provider's implementation for what can be provided
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
AsyncGenerator[ChatCompletionChunk, None]
|
56
|
+
"""
|
57
|
+
request.stream = True
|
58
|
+
if provider == "openai" or provider == "azure_openai":
|
59
|
+
async for chunk in openai_chat_completion_stream(request, client):
|
60
|
+
yield chunk
|
61
|
+
elif provider == "ollama":
|
62
|
+
async for chunk in ollama_chat_completion_stream(request, client):
|
63
|
+
yield chunk
|
31
64
|
else:
|
32
65
|
raise ValueError(f"Provider {provider} not supported")
|
@@ -0,0 +1,180 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from anthropic import Anthropic
|
7
|
+
from anthropic.types import Message
|
8
|
+
|
9
|
+
from not_again_ai.llm.chat_completion.types import (
|
10
|
+
AssistantMessage,
|
11
|
+
ChatCompletionChoice,
|
12
|
+
ChatCompletionRequest,
|
13
|
+
ChatCompletionResponse,
|
14
|
+
Function,
|
15
|
+
ToolCall,
|
16
|
+
)
|
17
|
+
|
18
|
+
ANTHROPIC_PARAMETER_MAP = {
|
19
|
+
"max_completion_tokens": "max_tokens",
|
20
|
+
}
|
21
|
+
|
22
|
+
|
23
|
+
def anthropic_chat_completion(request: ChatCompletionRequest, client: Callable[..., Any]) -> ChatCompletionResponse:
|
24
|
+
"""Anthropic chat completion function.
|
25
|
+
|
26
|
+
TODO
|
27
|
+
- Image messages
|
28
|
+
- Thinking
|
29
|
+
- Citations
|
30
|
+
- Stop sequences
|
31
|
+
- Documents
|
32
|
+
"""
|
33
|
+
kwargs = request.model_dump(mode="json", exclude_none=True)
|
34
|
+
|
35
|
+
# For each key in ANTHROPIC_PARAMETER_MAP
|
36
|
+
# If it is not None, set the key in kwargs to the value of the corresponding value in ANTHROPIC_PARAMETER_MAP
|
37
|
+
# If it is None, remove that key from kwargs
|
38
|
+
for key, value in ANTHROPIC_PARAMETER_MAP.items():
|
39
|
+
if value is not None and key in kwargs:
|
40
|
+
kwargs[value] = kwargs.pop(key)
|
41
|
+
elif value is None and key in kwargs:
|
42
|
+
del kwargs[key]
|
43
|
+
|
44
|
+
# Handle messages
|
45
|
+
# Any system messages need to be removed from messages and concatenated into a single string (in order)
|
46
|
+
# Any tool messages need to be converted to a special user message
|
47
|
+
# Any assistant messages with tool calls need to be converted.
|
48
|
+
system = ""
|
49
|
+
new_messages = []
|
50
|
+
for message in kwargs["messages"]:
|
51
|
+
if message["role"] == "system":
|
52
|
+
system += message["content"] + "\n"
|
53
|
+
elif message["role"] == "tool":
|
54
|
+
new_messages.append(
|
55
|
+
{
|
56
|
+
"role": "user",
|
57
|
+
"content": [
|
58
|
+
{
|
59
|
+
"type": "tool_result",
|
60
|
+
"tool_use_id": message["name"],
|
61
|
+
"content": message["content"],
|
62
|
+
}
|
63
|
+
],
|
64
|
+
}
|
65
|
+
)
|
66
|
+
elif message["role"] == "assistant":
|
67
|
+
content = []
|
68
|
+
if message.get("content", None):
|
69
|
+
content.append(
|
70
|
+
{
|
71
|
+
"type": "text",
|
72
|
+
"content": message["content"],
|
73
|
+
}
|
74
|
+
)
|
75
|
+
for tool_call in message.get("tool_calls", []):
|
76
|
+
content.append(
|
77
|
+
{
|
78
|
+
"type": "tool_use",
|
79
|
+
"id": tool_call["id"],
|
80
|
+
"name": tool_call["function"]["name"],
|
81
|
+
"input": tool_call["function"]["arguments"],
|
82
|
+
}
|
83
|
+
)
|
84
|
+
new_messages.append(
|
85
|
+
{
|
86
|
+
"role": "assistant",
|
87
|
+
"content": content,
|
88
|
+
}
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
new_messages.append(message)
|
92
|
+
kwargs["messages"] = new_messages
|
93
|
+
system = system.strip()
|
94
|
+
if system:
|
95
|
+
kwargs["system"] = system
|
96
|
+
|
97
|
+
# Handle tool choice and parallel tool calls
|
98
|
+
if kwargs.get("tool_choice") is not None:
|
99
|
+
tool_choice_value = kwargs.pop("tool_choice")
|
100
|
+
tool_choice = {}
|
101
|
+
if tool_choice_value == "none":
|
102
|
+
tool_choice["type"] = "none"
|
103
|
+
elif tool_choice_value in ["auto", "any"]:
|
104
|
+
tool_choice["type"] = "auto"
|
105
|
+
if kwargs.get("parallel_tool_calls") is not None:
|
106
|
+
tool_choice["disable_parallel_tool_use"] = str(not kwargs["parallel_tool_calls"])
|
107
|
+
else:
|
108
|
+
tool_choice["name"] = tool_choice_value
|
109
|
+
tool_choice["type"] = "tool"
|
110
|
+
if kwargs.get("parallel_tool_calls") is not None:
|
111
|
+
tool_choice["disable_parallel_tool_use"] = str(not kwargs["parallel_tool_calls"])
|
112
|
+
kwargs["tool_choice"] = tool_choice
|
113
|
+
kwargs.pop("parallel_tool_calls", None)
|
114
|
+
|
115
|
+
start_time = time.time()
|
116
|
+
response: Message = client(**kwargs)
|
117
|
+
end_time = time.time()
|
118
|
+
response_duration = round(end_time - start_time, 4)
|
119
|
+
|
120
|
+
tool_calls: list[ToolCall] = []
|
121
|
+
assistant_message = ""
|
122
|
+
for block in response.content:
|
123
|
+
if block.type == "text":
|
124
|
+
assistant_message += block.text
|
125
|
+
elif block.type == "tool_use":
|
126
|
+
tool_calls.append(
|
127
|
+
ToolCall(
|
128
|
+
id=block.id,
|
129
|
+
function=Function(
|
130
|
+
name=block.name,
|
131
|
+
arguments=block.input, # type: ignore
|
132
|
+
),
|
133
|
+
)
|
134
|
+
)
|
135
|
+
|
136
|
+
choice = ChatCompletionChoice(
|
137
|
+
message=AssistantMessage(
|
138
|
+
content=assistant_message,
|
139
|
+
tool_calls=tool_calls,
|
140
|
+
),
|
141
|
+
finish_reason=response.stop_reason or "stop",
|
142
|
+
)
|
143
|
+
|
144
|
+
chat_completion_response = ChatCompletionResponse(
|
145
|
+
choices=[choice],
|
146
|
+
errors="",
|
147
|
+
completion_tokens=response.usage.output_tokens,
|
148
|
+
prompt_tokens=response.usage.input_tokens,
|
149
|
+
cache_read_input_tokens=response.usage.cache_read_input_tokens,
|
150
|
+
cache_creation_input_tokens=response.usage.cache_creation_input_tokens,
|
151
|
+
response_duration=response_duration,
|
152
|
+
)
|
153
|
+
return chat_completion_response
|
154
|
+
|
155
|
+
|
156
|
+
def create_client_callable(client_class: type[Anthropic], **client_args: Any) -> Callable[..., Any]:
|
157
|
+
"""Creates a callable that instantiates and uses an Anthropic client.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
client_class: The Anthropic client class to instantiate
|
161
|
+
**client_args: Arguments to pass to the client constructor
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
A callable that creates a client and returns completion results
|
165
|
+
"""
|
166
|
+
filtered_args = {k: v for k, v in client_args.items() if v is not None}
|
167
|
+
|
168
|
+
def client_callable(**kwargs: Any) -> Any:
|
169
|
+
client = client_class(**filtered_args)
|
170
|
+
completion = client.beta.messages.create(**kwargs)
|
171
|
+
return completion
|
172
|
+
|
173
|
+
return client_callable
|
174
|
+
|
175
|
+
|
176
|
+
def anthropic_client(api_key: str | None = None) -> Callable[..., Any]:
|
177
|
+
if not api_key:
|
178
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
179
|
+
client_callable = create_client_callable(Anthropic, api_key=api_key)
|
180
|
+
return client_callable
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from collections.abc import Callable
|
1
|
+
from collections.abc import AsyncGenerator, Callable
|
2
2
|
import json
|
3
3
|
import os
|
4
4
|
import re
|
@@ -6,14 +6,20 @@ import time
|
|
6
6
|
from typing import Any, Literal, cast
|
7
7
|
|
8
8
|
from loguru import logger
|
9
|
-
from ollama import ChatResponse, Client, ResponseError
|
9
|
+
from ollama import AsyncClient, ChatResponse, Client, ResponseError
|
10
10
|
|
11
11
|
from not_again_ai.llm.chat_completion.types import (
|
12
12
|
AssistantMessage,
|
13
13
|
ChatCompletionChoice,
|
14
|
+
ChatCompletionChoiceStream,
|
15
|
+
ChatCompletionChunk,
|
16
|
+
ChatCompletionDelta,
|
14
17
|
ChatCompletionRequest,
|
15
18
|
ChatCompletionResponse,
|
16
19
|
Function,
|
20
|
+
PartialFunction,
|
21
|
+
PartialToolCall,
|
22
|
+
Role,
|
17
23
|
ToolCall,
|
18
24
|
)
|
19
25
|
|
@@ -51,14 +57,8 @@ def validate(request: ChatCompletionRequest) -> None:
|
|
51
57
|
raise ValueError("`max_tokens` and `max_completion_tokens` cannot both be provided.")
|
52
58
|
|
53
59
|
|
54
|
-
def
|
55
|
-
request: ChatCompletionRequest,
|
56
|
-
client: Callable[..., Any],
|
57
|
-
) -> ChatCompletionResponse:
|
58
|
-
validate(request)
|
59
|
-
|
60
|
+
def format_kwargs(request: ChatCompletionRequest) -> dict[str, Any]:
|
60
61
|
kwargs = request.model_dump(mode="json", exclude_none=True)
|
61
|
-
|
62
62
|
# For each key in OLLAMA_PARAMETER_MAP
|
63
63
|
# If it is not None, set the key in kwargs to the value of the corresponding value in OLLAMA_PARAMETER_MAP
|
64
64
|
# If it is None, remove that key from kwargs
|
@@ -141,6 +141,16 @@ def ollama_chat_completion(
|
|
141
141
|
logger.warning("Ollama model only supports a single image per message. Using only the first images.")
|
142
142
|
message["images"] = images
|
143
143
|
|
144
|
+
return kwargs
|
145
|
+
|
146
|
+
|
147
|
+
def ollama_chat_completion(
|
148
|
+
request: ChatCompletionRequest,
|
149
|
+
client: Callable[..., Any],
|
150
|
+
) -> ChatCompletionResponse:
|
151
|
+
validate(request)
|
152
|
+
kwargs = format_kwargs(request)
|
153
|
+
|
144
154
|
try:
|
145
155
|
start_time = time.time()
|
146
156
|
response: ChatResponse = client(**kwargs)
|
@@ -164,7 +174,7 @@ def ollama_chat_completion(
|
|
164
174
|
tool_name = tool_call.function.name
|
165
175
|
if request.tools and tool_name not in [tool["function"]["name"] for tool in request.tools]:
|
166
176
|
errors += f"Tool call {tool_call} has an invalid tool name: {tool_name}\n"
|
167
|
-
tool_args = tool_call.function.arguments
|
177
|
+
tool_args = dict(tool_call.function.arguments)
|
168
178
|
parsed_tool_calls.append(
|
169
179
|
ToolCall(
|
170
180
|
id="",
|
@@ -206,7 +216,65 @@ def ollama_chat_completion(
|
|
206
216
|
)
|
207
217
|
|
208
218
|
|
209
|
-
def
|
219
|
+
async def ollama_chat_completion_stream(
|
220
|
+
request: ChatCompletionRequest,
|
221
|
+
client: Callable[..., Any],
|
222
|
+
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
223
|
+
validate(request)
|
224
|
+
kwargs = format_kwargs(request)
|
225
|
+
|
226
|
+
start_time = time.time()
|
227
|
+
stream = await client(**kwargs)
|
228
|
+
|
229
|
+
async for chunk in stream:
|
230
|
+
errors = ""
|
231
|
+
# Handle tool calls
|
232
|
+
tool_calls: list[PartialToolCall] | None = None
|
233
|
+
if chunk.message.tool_calls:
|
234
|
+
parsed_tool_calls: list[PartialToolCall] = []
|
235
|
+
for tool_call in chunk.message.tool_calls:
|
236
|
+
tool_name = tool_call.function.name
|
237
|
+
if request.tools and tool_name not in [tool["function"]["name"] for tool in request.tools]:
|
238
|
+
errors += f"Tool call {tool_call} has an invalid tool name: {tool_name}\n"
|
239
|
+
tool_args = tool_call.function.arguments
|
240
|
+
|
241
|
+
parsed_tool_calls.append(
|
242
|
+
PartialToolCall(
|
243
|
+
id="",
|
244
|
+
function=PartialFunction(
|
245
|
+
name=tool_name,
|
246
|
+
arguments=tool_args,
|
247
|
+
),
|
248
|
+
)
|
249
|
+
)
|
250
|
+
tool_calls = parsed_tool_calls
|
251
|
+
|
252
|
+
current_time = time.time()
|
253
|
+
response_duration = round(current_time - start_time, 4)
|
254
|
+
|
255
|
+
delta = ChatCompletionDelta(
|
256
|
+
content=chunk.message.content or "",
|
257
|
+
role=Role.ASSISTANT,
|
258
|
+
tool_calls=tool_calls,
|
259
|
+
)
|
260
|
+
choice_obj = ChatCompletionChoiceStream(
|
261
|
+
delta=delta,
|
262
|
+
finish_reason=chunk.done_reason,
|
263
|
+
index=0,
|
264
|
+
)
|
265
|
+
chunk_obj = ChatCompletionChunk(
|
266
|
+
choices=[choice_obj],
|
267
|
+
errors=errors.strip(),
|
268
|
+
completion_tokens=chunk.get("eval_count", None),
|
269
|
+
prompt_tokens=chunk.get("prompt_eval_count", None),
|
270
|
+
response_duration=response_duration,
|
271
|
+
)
|
272
|
+
yield chunk_obj
|
273
|
+
|
274
|
+
|
275
|
+
def ollama_client(
|
276
|
+
host: str | None = None, timeout: float | None = None, async_client: bool = False
|
277
|
+
) -> Callable[..., Any]:
|
210
278
|
"""Create an Ollama client instance based on the specified host or will read from the OLLAMA_HOST environment variable.
|
211
279
|
|
212
280
|
Args:
|
@@ -226,7 +294,7 @@ def ollama_client(host: str | None = None, timeout: float | None = None) -> Call
|
|
226
294
|
host = "http://localhost:11434"
|
227
295
|
|
228
296
|
def client_callable(**kwargs: Any) -> Any:
|
229
|
-
client = Client(host=host, timeout=timeout)
|
297
|
+
client = AsyncClient(host=host, timeout=timeout) if async_client else Client(host=host, timeout=timeout)
|
230
298
|
return client.chat(**kwargs)
|
231
299
|
|
232
300
|
return client_callable
|
@@ -1,17 +1,23 @@
|
|
1
|
-
from collections.abc import Callable
|
1
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine
|
2
2
|
import json
|
3
3
|
import time
|
4
4
|
from typing import Any, Literal
|
5
5
|
|
6
6
|
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
|
7
|
-
from openai import AzureOpenAI, OpenAI
|
7
|
+
from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI
|
8
8
|
|
9
9
|
from not_again_ai.llm.chat_completion.types import (
|
10
10
|
AssistantMessage,
|
11
11
|
ChatCompletionChoice,
|
12
|
+
ChatCompletionChoiceStream,
|
13
|
+
ChatCompletionChunk,
|
14
|
+
ChatCompletionDelta,
|
12
15
|
ChatCompletionRequest,
|
13
16
|
ChatCompletionResponse,
|
14
17
|
Function,
|
18
|
+
PartialFunction,
|
19
|
+
PartialToolCall,
|
20
|
+
Role,
|
15
21
|
ToolCall,
|
16
22
|
)
|
17
23
|
|
@@ -36,12 +42,7 @@ def validate(request: ChatCompletionRequest) -> None:
|
|
36
42
|
raise ValueError("`max_tokens` and `max_completion_tokens` cannot both be provided.")
|
37
43
|
|
38
44
|
|
39
|
-
def
|
40
|
-
request: ChatCompletionRequest,
|
41
|
-
client: Callable[..., Any],
|
42
|
-
) -> ChatCompletionResponse:
|
43
|
-
validate(request)
|
44
|
-
|
45
|
+
def format_kwargs(request: ChatCompletionRequest) -> dict[str, Any]:
|
45
46
|
# Format the response format parameters to be compatible with OpenAI API
|
46
47
|
if request.json_mode:
|
47
48
|
response_format: dict[str, Any] = {"type": "json_object"}
|
@@ -61,7 +62,6 @@ def openai_chat_completion(
|
|
61
62
|
elif value is None and key in kwargs:
|
62
63
|
del kwargs[key]
|
63
64
|
|
64
|
-
# Iterate over each message and
|
65
65
|
for message in kwargs["messages"]:
|
66
66
|
role = message.get("role", None)
|
67
67
|
# For each ToolMessage, change the "name" field to be named "tool_call_id" instead
|
@@ -84,6 +84,49 @@ def openai_chat_completion(
|
|
84
84
|
if request.tool_choice is not None and request.tool_choice not in ["none", "auto", "required"]:
|
85
85
|
kwargs["tool_choice"] = {"type": "function", "function": {"name": request.tool_choice}}
|
86
86
|
|
87
|
+
return kwargs
|
88
|
+
|
89
|
+
|
90
|
+
def process_logprobs(logprobs_content: list[dict[str, Any]]) -> list[dict[str, Any] | list[dict[str, Any]]]:
|
91
|
+
"""Process logprobs content from OpenAI API response.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
logprobs_content: List of logprob entries from the API response
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Processed logprobs list containing either single token info or lists of top token infos
|
98
|
+
"""
|
99
|
+
logprobs_list: list[dict[str, Any] | list[dict[str, Any]]] = []
|
100
|
+
for logprob in logprobs_content:
|
101
|
+
if logprob.get("top_logprobs", None):
|
102
|
+
curr_logprob_infos: list[dict[str, Any]] = []
|
103
|
+
for top_logprob in logprob.get("top_logprobs", []):
|
104
|
+
curr_logprob_infos.append(
|
105
|
+
{
|
106
|
+
"token": top_logprob.get("token", ""),
|
107
|
+
"logprob": top_logprob.get("logprob", 0),
|
108
|
+
"bytes": top_logprob.get("bytes", 0),
|
109
|
+
}
|
110
|
+
)
|
111
|
+
logprobs_list.append(curr_logprob_infos)
|
112
|
+
else:
|
113
|
+
logprobs_list.append(
|
114
|
+
{
|
115
|
+
"token": logprob.get("token", ""),
|
116
|
+
"logprob": logprob.get("logprob", 0),
|
117
|
+
"bytes": logprob.get("bytes", 0),
|
118
|
+
}
|
119
|
+
)
|
120
|
+
return logprobs_list
|
121
|
+
|
122
|
+
|
123
|
+
def openai_chat_completion(
|
124
|
+
request: ChatCompletionRequest,
|
125
|
+
client: Callable[..., Any],
|
126
|
+
) -> ChatCompletionResponse:
|
127
|
+
validate(request)
|
128
|
+
kwargs = format_kwargs(request)
|
129
|
+
|
87
130
|
start_time = time.time()
|
88
131
|
response = client(**kwargs)
|
89
132
|
end_time = time.time()
|
@@ -133,28 +176,7 @@ def openai_chat_completion(
|
|
133
176
|
# Handle logprobs
|
134
177
|
logprobs: list[dict[str, Any] | list[dict[str, Any]]] | None = None
|
135
178
|
if choice.get("logprobs", None) and choice["logprobs"].get("content", None) is not None:
|
136
|
-
|
137
|
-
for logprob in choice["logprobs"]["content"]:
|
138
|
-
if logprob.get("top_logprobs", None):
|
139
|
-
curr_logprob_infos: list[dict[str, Any]] = []
|
140
|
-
for top_logprob in logprob.get("top_logprobs", []):
|
141
|
-
curr_logprob_infos.append(
|
142
|
-
{
|
143
|
-
"token": top_logprob.get("token", ""),
|
144
|
-
"logprob": top_logprob.get("logprob", 0),
|
145
|
-
"bytes": top_logprob.get("bytes", 0),
|
146
|
-
}
|
147
|
-
)
|
148
|
-
logprobs_list.append(curr_logprob_infos)
|
149
|
-
else:
|
150
|
-
logprobs_list.append(
|
151
|
-
{
|
152
|
-
"token": logprob.get("token", ""),
|
153
|
-
"logprob": logprob.get("logprob", 0),
|
154
|
-
"bytes": logprob.get("bytes", 0),
|
155
|
-
}
|
156
|
-
)
|
157
|
-
logprobs = logprobs_list
|
179
|
+
logprobs = process_logprobs(choice["logprobs"]["content"])
|
158
180
|
|
159
181
|
# Handle extras that OpenAI or Azure OpenAI return
|
160
182
|
if choice.get("content_filter_results", None):
|
@@ -195,6 +217,107 @@ def openai_chat_completion(
|
|
195
217
|
)
|
196
218
|
|
197
219
|
|
220
|
+
async def openai_chat_completion_stream(
|
221
|
+
request: ChatCompletionRequest,
|
222
|
+
client: Callable[..., Any],
|
223
|
+
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
224
|
+
validate(request)
|
225
|
+
kwargs = format_kwargs(request)
|
226
|
+
|
227
|
+
start_time = time.time()
|
228
|
+
stream = await client(**kwargs)
|
229
|
+
|
230
|
+
async for chunk in stream:
|
231
|
+
errors = ""
|
232
|
+
# This kind of a hack. To make this processing generic for clients that do not return the correct
|
233
|
+
# data structure, we convert the chunk to a dict
|
234
|
+
if not isinstance(chunk, dict):
|
235
|
+
chunk = chunk.to_dict()
|
236
|
+
|
237
|
+
choices: list[ChatCompletionChoiceStream] = []
|
238
|
+
for choice in chunk["choices"]:
|
239
|
+
content = choice.get("delta", {}).get("content", "")
|
240
|
+
if not content:
|
241
|
+
content = ""
|
242
|
+
|
243
|
+
role = Role.ASSISTANT
|
244
|
+
if choice.get("delta", {}).get("role", None):
|
245
|
+
role = Role(choice["delta"]["role"])
|
246
|
+
|
247
|
+
# Handle tool calls
|
248
|
+
tool_calls: list[PartialToolCall] | None = None
|
249
|
+
if choice["delta"].get("tool_calls", None):
|
250
|
+
parsed_tool_calls: list[PartialToolCall] = []
|
251
|
+
for tool_call in choice["delta"]["tool_calls"]:
|
252
|
+
tool_name = tool_call.get("function", {}).get("name", None)
|
253
|
+
if not tool_name:
|
254
|
+
tool_name = ""
|
255
|
+
tool_args = tool_call.get("function", {}).get("arguments", "")
|
256
|
+
if not tool_args:
|
257
|
+
tool_args = ""
|
258
|
+
|
259
|
+
tool_id = tool_call.get("id", None)
|
260
|
+
parsed_tool_calls.append(
|
261
|
+
PartialToolCall(
|
262
|
+
id=tool_id,
|
263
|
+
function=PartialFunction(
|
264
|
+
name=tool_name,
|
265
|
+
arguments=tool_args,
|
266
|
+
),
|
267
|
+
)
|
268
|
+
)
|
269
|
+
tool_calls = parsed_tool_calls
|
270
|
+
|
271
|
+
refusal = None
|
272
|
+
if choice["delta"].get("refusal", None):
|
273
|
+
refusal = choice["delta"]["refusal"]
|
274
|
+
|
275
|
+
delta = ChatCompletionDelta(
|
276
|
+
content=content,
|
277
|
+
role=role,
|
278
|
+
tool_calls=tool_calls,
|
279
|
+
refusal=refusal,
|
280
|
+
)
|
281
|
+
|
282
|
+
index = choice.get("index", 0)
|
283
|
+
finish_reason = choice.get("finish_reason", None)
|
284
|
+
|
285
|
+
# Handle logprobs
|
286
|
+
logprobs: list[dict[str, Any] | list[dict[str, Any]]] | None = None
|
287
|
+
if choice.get("logprobs", None) and choice["logprobs"].get("content", None) is not None:
|
288
|
+
logprobs = process_logprobs(choice["logprobs"]["content"])
|
289
|
+
|
290
|
+
choice_obj = ChatCompletionChoiceStream(
|
291
|
+
delta=delta,
|
292
|
+
finish_reason=finish_reason,
|
293
|
+
logprobs=logprobs,
|
294
|
+
index=index,
|
295
|
+
)
|
296
|
+
choices.append(choice_obj)
|
297
|
+
|
298
|
+
current_time = time.time()
|
299
|
+
response_duration = round(current_time - start_time, 4)
|
300
|
+
|
301
|
+
if "usage" in chunk and chunk["usage"] is not None:
|
302
|
+
completion_tokens = chunk["usage"].get("completion_tokens", None)
|
303
|
+
prompt_tokens = chunk["usage"].get("prompt_tokens", None)
|
304
|
+
system_fingerprint = chunk.get("system_fingerprint", None)
|
305
|
+
else:
|
306
|
+
completion_tokens = None
|
307
|
+
prompt_tokens = None
|
308
|
+
system_fingerprint = None
|
309
|
+
|
310
|
+
chunk_obj = ChatCompletionChunk(
|
311
|
+
choices=choices,
|
312
|
+
errors=errors.strip(),
|
313
|
+
completion_tokens=completion_tokens,
|
314
|
+
prompt_tokens=prompt_tokens,
|
315
|
+
response_duration=response_duration,
|
316
|
+
system_fingerprint=system_fingerprint,
|
317
|
+
)
|
318
|
+
yield chunk_obj
|
319
|
+
|
320
|
+
|
198
321
|
def create_client_callable(client_class: type[OpenAI | AzureOpenAI], **client_args: Any) -> Callable[..., Any]:
|
199
322
|
"""Creates a callable that instantiates and uses an OpenAI client.
|
200
323
|
|
@@ -215,6 +338,20 @@ def create_client_callable(client_class: type[OpenAI | AzureOpenAI], **client_ar
|
|
215
338
|
return client_callable
|
216
339
|
|
217
340
|
|
341
|
+
def create_client_callable_stream(
|
342
|
+
client_class: type[AsyncOpenAI | AsyncAzureOpenAI], **client_args: Any
|
343
|
+
) -> Callable[..., Any]:
|
344
|
+
filtered_args = {k: v for k, v in client_args.items() if v is not None}
|
345
|
+
|
346
|
+
def client_callable(**kwargs: Any) -> Coroutine[Any, Any, Any]:
|
347
|
+
client = client_class(**filtered_args)
|
348
|
+
kwargs["stream_options"] = {"include_usage": True}
|
349
|
+
stream = client.chat.completions.create(**kwargs)
|
350
|
+
return stream
|
351
|
+
|
352
|
+
return client_callable
|
353
|
+
|
354
|
+
|
218
355
|
class InvalidOAIAPITypeError(Exception):
|
219
356
|
"""Raised when an invalid OAIAPIType string is provided."""
|
220
357
|
|
@@ -227,6 +364,7 @@ def openai_client(
|
|
227
364
|
azure_endpoint: str | None = None,
|
228
365
|
timeout: float | None = None,
|
229
366
|
max_retries: int | None = None,
|
367
|
+
async_client: bool = False,
|
230
368
|
) -> Callable[..., Any]:
|
231
369
|
"""Create an OpenAI or Azure OpenAI client instance based on the specified API type and other provided parameters.
|
232
370
|
|
@@ -247,11 +385,11 @@ def openai_client(
|
|
247
385
|
max_retries (int, optional): Certain errors are automatically retried 2 times by default,
|
248
386
|
with a short exponential backoff. Connection errors (for example, due to a network connectivity problem),
|
249
387
|
408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default.
|
388
|
+
async_client (bool, optional): Whether to return an async client. Defaults to False.
|
250
389
|
|
251
390
|
Returns:
|
252
391
|
Callable[..., Any]: A callable that creates a client and returns completion results
|
253
392
|
|
254
|
-
|
255
393
|
Raises:
|
256
394
|
InvalidOAIAPITypeError: If an invalid API type string is provided.
|
257
395
|
NotImplementedError: If the specified API type is recognized but not yet supported (e.g., 'azure_openai').
|
@@ -260,17 +398,21 @@ def openai_client(
|
|
260
398
|
raise InvalidOAIAPITypeError(f"Invalid OAIAPIType: {api_type}. Must be 'openai' or 'azure_openai'.")
|
261
399
|
|
262
400
|
if api_type == "openai":
|
263
|
-
|
264
|
-
|
401
|
+
client_class = AsyncOpenAI if async_client else OpenAI
|
402
|
+
callable_creator = create_client_callable_stream if async_client else create_client_callable
|
403
|
+
return callable_creator(
|
404
|
+
client_class, # type: ignore
|
265
405
|
api_key=api_key,
|
266
406
|
organization=organization,
|
267
407
|
timeout=timeout,
|
268
408
|
max_retries=max_retries,
|
269
409
|
)
|
270
410
|
elif api_type == "azure_openai":
|
411
|
+
azure_client_class = AsyncAzureOpenAI if async_client else AzureOpenAI
|
412
|
+
callable_creator = create_client_callable_stream if async_client else create_client_callable
|
271
413
|
if api_key:
|
272
|
-
return
|
273
|
-
|
414
|
+
return callable_creator(
|
415
|
+
azure_client_class, # type: ignore
|
274
416
|
api_version=aoai_api_version,
|
275
417
|
azure_endpoint=azure_endpoint,
|
276
418
|
api_key=api_key,
|
@@ -282,8 +424,8 @@ def openai_client(
|
|
282
424
|
ad_token_provider = get_bearer_token_provider(
|
283
425
|
azure_credential, "https://cognitiveservices.azure.com/.default"
|
284
426
|
)
|
285
|
-
return
|
286
|
-
|
427
|
+
return callable_creator(
|
428
|
+
azure_client_class, # type: ignore
|
287
429
|
api_version=aoai_api_version,
|
288
430
|
azure_endpoint=azure_endpoint,
|
289
431
|
azure_ad_token_provider=ad_token_provider,
|
@@ -52,12 +52,23 @@ class Function(BaseModel):
|
|
52
52
|
arguments: dict[str, Any]
|
53
53
|
|
54
54
|
|
55
|
+
class PartialFunction(BaseModel):
|
56
|
+
name: str
|
57
|
+
arguments: str | dict[str, Any]
|
58
|
+
|
59
|
+
|
55
60
|
class ToolCall(BaseModel):
|
56
61
|
id: str
|
57
62
|
function: Function
|
58
63
|
type: Literal["function"] = "function"
|
59
64
|
|
60
65
|
|
66
|
+
class PartialToolCall(BaseModel):
|
67
|
+
id: str | None
|
68
|
+
function: PartialFunction
|
69
|
+
type: Literal["function"] = "function"
|
70
|
+
|
71
|
+
|
61
72
|
class DeveloperMessage(BaseMessage[str]):
|
62
73
|
role: Literal[Role.DEVELOPER] = Role.DEVELOPER
|
63
74
|
|
@@ -87,6 +98,7 @@ MessageT = AssistantMessage | DeveloperMessage | SystemMessage | ToolMessage | U
|
|
87
98
|
class ChatCompletionRequest(BaseModel):
|
88
99
|
messages: list[MessageT]
|
89
100
|
model: str
|
101
|
+
stream: bool = Field(default=False)
|
90
102
|
|
91
103
|
max_completion_tokens: int | None = Field(default=None)
|
92
104
|
context_window: int | None = Field(default=None)
|
@@ -126,7 +138,9 @@ class ChatCompletionRequest(BaseModel):
|
|
126
138
|
|
127
139
|
class ChatCompletionChoice(BaseModel):
|
128
140
|
message: AssistantMessage
|
129
|
-
finish_reason: Literal[
|
141
|
+
finish_reason: Literal[
|
142
|
+
"stop", "length", "tool_calls", "content_filter", "end_turn", "max_tokens", "stop_sequence", "tool_use"
|
143
|
+
]
|
130
144
|
|
131
145
|
json_message: dict[str, Any] | None = Field(default=None)
|
132
146
|
logprobs: list[dict[str, Any] | list[dict[str, Any]]] | None = Field(default=None)
|
@@ -143,8 +157,42 @@ class ChatCompletionResponse(BaseModel):
|
|
143
157
|
prompt_tokens: int
|
144
158
|
completion_detailed_tokens: dict[str, int] | None = Field(default=None)
|
145
159
|
prompt_detailed_tokens: dict[str, int] | None = Field(default=None)
|
160
|
+
cache_read_input_tokens: int | None = Field(default=None)
|
161
|
+
cache_creation_input_tokens: int | None = Field(default=None)
|
146
162
|
response_duration: float
|
147
163
|
|
148
164
|
system_fingerprint: str | None = Field(default=None)
|
149
165
|
|
150
166
|
extras: Any | None = Field(default=None)
|
167
|
+
|
168
|
+
|
169
|
+
class ChatCompletionDelta(BaseModel):
|
170
|
+
content: str
|
171
|
+
role: Role = Field(default=Role.ASSISTANT)
|
172
|
+
|
173
|
+
tool_calls: list[PartialToolCall] | None = Field(default=None)
|
174
|
+
|
175
|
+
refusal: str | None = Field(default=None)
|
176
|
+
|
177
|
+
|
178
|
+
class ChatCompletionChoiceStream(BaseModel):
|
179
|
+
delta: ChatCompletionDelta
|
180
|
+
index: int
|
181
|
+
finish_reason: Literal["stop", "length", "tool_calls", "content_filter"] | None
|
182
|
+
|
183
|
+
logprobs: list[dict[str, Any] | list[dict[str, Any]]] | None = Field(default=None)
|
184
|
+
|
185
|
+
extras: Any | None = Field(default=None)
|
186
|
+
|
187
|
+
|
188
|
+
class ChatCompletionChunk(BaseModel):
|
189
|
+
choices: list[ChatCompletionChoiceStream]
|
190
|
+
|
191
|
+
errors: str = Field(default="")
|
192
|
+
|
193
|
+
completion_tokens: int | None = Field(default=None)
|
194
|
+
prompt_tokens: int | None = Field(default=None)
|
195
|
+
response_duration: float | None = Field(default=None)
|
196
|
+
|
197
|
+
system_fingerprint: str | None = Field(default=None)
|
198
|
+
extras: Any | None = Field(default=None)
|
@@ -5,7 +5,7 @@ import mimetypes
|
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import Any
|
7
7
|
|
8
|
-
from liquid import
|
8
|
+
from liquid import render
|
9
9
|
from openai.lib._pydantic import to_strict_json_schema
|
10
10
|
from pydantic import BaseModel
|
11
11
|
|
@@ -15,7 +15,7 @@ from not_again_ai.llm.chat_completion.types import MessageT
|
|
15
15
|
def _apply_templates(value: Any, variables: dict[str, str]) -> Any:
|
16
16
|
"""Recursively applies Liquid templating to all string fields within the given value."""
|
17
17
|
if isinstance(value, str):
|
18
|
-
return
|
18
|
+
return render(value, **variables)
|
19
19
|
elif isinstance(value, list):
|
20
20
|
return [_apply_templates(item, variables) for item in value]
|
21
21
|
elif isinstance(value, dict):
|
@@ -31,7 +31,7 @@ def _apply_templates(value: Any, variables: dict[str, str]) -> Any:
|
|
31
31
|
|
32
32
|
def compile_messages(messages: Sequence[MessageT], variables: dict[str, str]) -> Sequence[MessageT]:
|
33
33
|
"""Compiles messages using Liquid templating and the provided variables.
|
34
|
-
Calls
|
34
|
+
Calls render(content_part, **variables) on each text content part.
|
35
35
|
|
36
36
|
Args:
|
37
37
|
messages: List of MessageT where content can contain Liquid templates.
|
@@ -1,11 +1,13 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: not-again-ai
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.18.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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
Project-URL: Homepage, https://github.com/DaveCoDev/not-again-ai
|
6
|
+
Project-URL: Documentation, https://davecodev.github.io/not-again-ai/
|
7
|
+
Project-URL: Repository, https://github.com/DaveCoDev/not-again-ai
|
8
|
+
Author-email: DaveCoDev <dave.co.dev@gmail.com>
|
9
|
+
License-Expression: MIT
|
10
|
+
License-File: LICENSE
|
9
11
|
Classifier: Development Status :: 3 - Alpha
|
10
12
|
Classifier: Intended Audience :: Developers
|
11
13
|
Classifier: Intended Audience :: Science/Research
|
@@ -16,52 +18,55 @@ Classifier: Programming Language :: Python :: 3
|
|
16
18
|
Classifier: Programming Language :: Python :: 3.11
|
17
19
|
Classifier: Programming Language :: Python :: 3.12
|
18
20
|
Classifier: Typing :: Typed
|
21
|
+
Requires-Python: >=3.11
|
22
|
+
Requires-Dist: loguru>=0.7
|
23
|
+
Requires-Dist: pydantic>=2.10
|
19
24
|
Provides-Extra: data
|
25
|
+
Requires-Dist: playwright<2.0,>=1.51; extra == 'data'
|
26
|
+
Requires-Dist: pytest-playwright<1.0,>=0.7; extra == 'data'
|
20
27
|
Provides-Extra: llm
|
28
|
+
Requires-Dist: anthropic<1.0,>=0.49; extra == 'llm'
|
29
|
+
Requires-Dist: azure-identity<2.0,>=1.21; extra == 'llm'
|
30
|
+
Requires-Dist: ollama<1.0,>=0.4; extra == 'llm'
|
31
|
+
Requires-Dist: openai<2.0,>=1.68; extra == 'llm'
|
32
|
+
Requires-Dist: python-liquid<3.0,>=2.0; extra == 'llm'
|
33
|
+
Requires-Dist: tiktoken<1.0,>=0.9; extra == 'llm'
|
21
34
|
Provides-Extra: statistics
|
35
|
+
Requires-Dist: numpy<3.0,>=2.2; extra == 'statistics'
|
36
|
+
Requires-Dist: scikit-learn<2.0,>=1.6; extra == 'statistics'
|
37
|
+
Requires-Dist: scipy>=1.15; extra == 'statistics'
|
22
38
|
Provides-Extra: viz
|
23
|
-
Requires-Dist:
|
24
|
-
Requires-Dist:
|
25
|
-
Requires-Dist:
|
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.50) ; 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
|
40
|
-
Project-URL: Repository, https://github.com/DaveCoDev/not-again-ai
|
39
|
+
Requires-Dist: numpy<3.0,>=2.2; extra == 'viz'
|
40
|
+
Requires-Dist: pandas<3.0,>=2.2; extra == 'viz'
|
41
|
+
Requires-Dist: seaborn<1.0,>=0.13; extra == 'viz'
|
41
42
|
Description-Content-Type: text/markdown
|
42
43
|
|
43
44
|
# not-again-ai
|
44
45
|
|
45
46
|
[![GitHub Actions][github-actions-badge]](https://github.com/johnthagen/python-blueprint/actions)
|
46
|
-
[![
|
47
|
+
[![uv][uv-badge]](https://github.com/astral-sh/uv)
|
47
48
|
[![Nox][nox-badge]](https://github.com/wntrblm/nox)
|
48
49
|
[![Ruff][ruff-badge]](https://github.com/astral-sh/ruff)
|
49
50
|
[![Type checked with mypy][mypy-badge]](https://mypy-lang.org/)
|
50
51
|
|
51
52
|
[github-actions-badge]: https://github.com/johnthagen/python-blueprint/workflows/python/badge.svg
|
52
|
-
[
|
53
|
+
[uv-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json
|
53
54
|
[nox-badge]: https://img.shields.io/badge/%F0%9F%A6%8A-Nox-D85E00.svg
|
54
55
|
[black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg
|
55
56
|
[ruff-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
56
57
|
[mypy-badge]: https://www.mypy-lang.org/static/mypy_badge.svg
|
57
58
|
|
58
|
-
**not-again-ai** is a collection of various building blocks that come up over and over again when developing AI products.
|
59
|
+
**not-again-ai** is a collection of various building blocks that come up over and over again when developing AI products.
|
60
|
+
The key goals of this package are to have simple, yet flexible interfaces and to minimize dependencies.
|
61
|
+
It is encouraged to also **a)** use this as a template for your own Python package.
|
62
|
+
**b)** instead of installing the package, copy and paste functions into your own projects.
|
63
|
+
We make this easier by limiting the number of dependencies and use an MIT license.
|
59
64
|
|
60
65
|
**Documentation** available within individual **[notebooks](notebooks)**, docstrings within the source, or auto-generated at [DaveCoDev.github.io/not-again-ai/](https://DaveCoDev.github.io/not-again-ai/).
|
61
66
|
|
62
67
|
# Installation
|
63
68
|
|
64
|
-
Requires: Python 3.11, or 3.12
|
69
|
+
Requires: Python 3.11, or 3.12 which can be installed with [uv](https://docs.astral.sh/uv/getting-started/installation/) by running the command `uv python install 3.12`
|
65
70
|
|
66
71
|
Install the entire package from [PyPI](https://pypi.org/project/not-again-ai/) with:
|
67
72
|
|
@@ -111,52 +116,35 @@ The package is split into subpackages, so you can install only the parts you nee
|
|
111
116
|
|
112
117
|
# Development Information
|
113
118
|
|
114
|
-
|
119
|
+
This package uses [uv](https://docs.astral.sh/uv/) to manage dependencies and
|
120
|
+
isolated [Python virtual environments](https://docs.python.org/3/library/venv.html).
|
115
121
|
|
116
|
-
|
117
|
-
|
118
|
-
|
122
|
+
To proceed,
|
123
|
+
[install uv globally](https://docs.astral.sh/uv/getting-started/installation/)
|
124
|
+
onto your system.
|
119
125
|
|
120
|
-
|
126
|
+
To install a specific version of Python:
|
121
127
|
|
122
|
-
```
|
123
|
-
|
124
|
-
```
|
125
|
-
|
126
|
-
(Optional) configure Poetry to use an in-project virtual environment.
|
127
|
-
```bash
|
128
|
-
$ poetry config virtualenvs.in-project true
|
128
|
+
```shell
|
129
|
+
uv python install 3.12
|
129
130
|
```
|
130
131
|
|
131
132
|
## Dependencies
|
132
133
|
|
133
134
|
Dependencies are defined in [`pyproject.toml`](./pyproject.toml) and specific versions are locked
|
134
|
-
into [`
|
135
|
+
into [`uv.lock`](./uv.lock). This allows for exact reproducible environments across
|
135
136
|
all machines that use the project, both during development and in production.
|
136
137
|
|
137
|
-
To
|
138
|
-
|
139
|
-
```bash
|
140
|
-
$ poetry update
|
141
|
-
```
|
142
|
-
|
143
|
-
To install all dependencies (with all extra dependencies) into an isolated virtual environment:
|
144
|
-
|
145
|
-
```bash
|
146
|
-
$ poetry sync --all-extras
|
147
|
-
```
|
138
|
+
To install all dependencies into an isolated virtual environment:
|
148
139
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
```bash
|
153
|
-
$ poetry shell
|
140
|
+
```shell
|
141
|
+
uv sync --all-extras
|
154
142
|
```
|
155
143
|
|
156
|
-
To
|
144
|
+
To upgrade all dependencies to their latest versions:
|
157
145
|
|
158
|
-
```
|
159
|
-
|
146
|
+
```shell
|
147
|
+
uv lock --upgrade
|
160
148
|
```
|
161
149
|
|
162
150
|
## Packaging
|
@@ -164,48 +152,40 @@ To deactivate the environment:
|
|
164
152
|
This project is designed as a Python package, meaning that it can be bundled up and redistributed
|
165
153
|
as a single compressed file.
|
166
154
|
|
167
|
-
Packaging is configured by
|
168
|
-
|
169
|
-
- [`pyproject.toml`](./pyproject.toml)
|
155
|
+
Packaging is configured by the [`pyproject.toml`](./pyproject.toml).
|
170
156
|
|
171
157
|
To package the project as both a
|
172
158
|
[source distribution](https://packaging.python.org/en/latest/flow/#the-source-distribution-sdist) and
|
173
159
|
a [wheel](https://packaging.python.org/en/latest/specifications/binary-distribution-format/):
|
174
160
|
|
175
161
|
```bash
|
176
|
-
$
|
162
|
+
$ uv build
|
177
163
|
```
|
178
164
|
|
179
165
|
This will generate `dist/not-again-ai-<version>.tar.gz` and `dist/not_again_ai-<version>-py3-none-any.whl`.
|
180
166
|
|
181
|
-
Read more about the [advantages of wheels](https://pythonwheels.com/) to understand why generating
|
182
|
-
wheel distributions are important.
|
183
167
|
|
184
168
|
## Publish Distributions to PyPI
|
185
169
|
|
186
170
|
Source and wheel redistributable packages can
|
187
|
-
be [published to PyPI](https://
|
171
|
+
be [published to PyPI](https://docs.astral.sh/uv/guides/package/) or installed
|
188
172
|
directly from the filesystem using `pip`.
|
189
173
|
|
190
|
-
```
|
191
|
-
|
174
|
+
```shell
|
175
|
+
uv publish
|
192
176
|
```
|
193
177
|
|
194
178
|
# Enforcing Code Quality
|
195
179
|
|
196
|
-
Automated code quality checks are performed using
|
197
|
-
|
198
|
-
[`
|
199
|
-
|
200
|
-
guide checking, type checking and documentation generation.
|
201
|
-
|
202
|
-
> Note: `nox` is installed into the virtual environment automatically by the `poetry sync`
|
203
|
-
> command above. Run `poetry shell` to activate the virtual environment.
|
180
|
+
Automated code quality checks are performed using [Nox](https://nox.thea.codes/en/stable/). Nox
|
181
|
+
will automatically create virtual environments and run commands based on
|
182
|
+
[`noxfile.py`](./noxfile.py) for unit testing, PEP 8 style guide checking, type checking and
|
183
|
+
documentation generation.
|
204
184
|
|
205
185
|
To run all default sessions:
|
206
186
|
|
207
|
-
```
|
208
|
-
|
187
|
+
```shell
|
188
|
+
uv run nox
|
209
189
|
```
|
210
190
|
|
211
191
|
## Unit Testing
|
@@ -237,7 +217,7 @@ pytest and code coverage are configured in [`pyproject.toml`](./pyproject.toml).
|
|
237
217
|
To run selected tests:
|
238
218
|
|
239
219
|
```bash
|
240
|
-
(.venv) $ nox -s test -- -k "test_web"
|
220
|
+
(.venv) $ uv run nox -s test -- -k "test_web"
|
241
221
|
```
|
242
222
|
|
243
223
|
## Code Style Checking
|
@@ -251,13 +231,13 @@ code. PEP 8 code compliance is verified using [Ruff][Ruff]. Ruff is configured i
|
|
251
231
|
To lint code, run:
|
252
232
|
|
253
233
|
```bash
|
254
|
-
(.venv) $ nox -s lint
|
234
|
+
(.venv) $ uv run nox -s lint
|
255
235
|
```
|
256
236
|
|
257
237
|
To automatically fix fixable lint errors, run:
|
258
238
|
|
259
239
|
```bash
|
260
|
-
(.venv) $ nox -s lint_fix
|
240
|
+
(.venv) $ uv run nox -s lint_fix
|
261
241
|
```
|
262
242
|
|
263
243
|
## Automated Code Formatting
|
@@ -267,13 +247,13 @@ To automatically fix fixable lint errors, run:
|
|
267
247
|
To automatically format code, run:
|
268
248
|
|
269
249
|
```bash
|
270
|
-
(.venv) $ nox -s fmt
|
250
|
+
(.venv) $ uv run nox -s fmt
|
271
251
|
```
|
272
252
|
|
273
253
|
To verify code has been formatted, such as in a CI job:
|
274
254
|
|
275
255
|
```bash
|
276
|
-
(.venv) $ nox -s fmt_check
|
256
|
+
(.venv) $ uv run nox -s fmt_check
|
277
257
|
```
|
278
258
|
|
279
259
|
## Type Checking
|
@@ -293,11 +273,9 @@ def factorial(n: int) -> int:
|
|
293
273
|
mypy is configured in [`pyproject.toml`](./pyproject.toml). To type check code, run:
|
294
274
|
|
295
275
|
```bash
|
296
|
-
(.venv) $ nox -s type_check
|
276
|
+
(.venv) $ uv run nox -s type_check
|
297
277
|
```
|
298
278
|
|
299
|
-
See also [awesome-python-typing](https://github.com/typeddjango/awesome-python-typing).
|
300
|
-
|
301
279
|
### Distributing Type Annotations
|
302
280
|
|
303
281
|
[PEP 561](https://www.python.org/dev/peps/pep-0561/) defines how a Python package should
|
@@ -313,7 +291,7 @@ installed package to indicate that inline type annotations should be checked.
|
|
313
291
|
Check for typos using [typos](https://github.com/crate-ci/typos)
|
314
292
|
|
315
293
|
```bash
|
316
|
-
(.venv) $ nox -s typos
|
294
|
+
(.venv) $ uv run nox -s typos
|
317
295
|
```
|
318
296
|
|
319
297
|
## Continuous Integration
|
@@ -331,81 +309,5 @@ Install the [Ruff extension](https://marketplace.visualstudio.com/items?itemName
|
|
331
309
|
|
332
310
|
Default settings are configured in [`.vscode/settings.json`](./.vscode/settings.json) which will enable Ruff with consistent settings.
|
333
311
|
|
334
|
-
# Generating Documentation
|
335
|
-
|
336
|
-
## Generating a User Guide
|
337
|
-
|
338
|
-
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) is a powerful static site
|
339
|
-
generator that combines easy-to-write Markdown, with a number of Markdown extensions that increase
|
340
|
-
the power of Markdown. This makes it a great fit for user guides and other technical documentation.
|
341
|
-
|
342
|
-
The example MkDocs project included in this project is configured to allow the built documentation
|
343
|
-
to be hosted at any URL or viewed offline from the file system.
|
344
|
-
|
345
|
-
To build the user guide, run,
|
346
|
-
|
347
|
-
```bash
|
348
|
-
(.venv) $ nox -s docs
|
349
|
-
```
|
350
|
-
|
351
|
-
and open `docs/user_guide/site/index.html` using a web browser.
|
352
|
-
|
353
|
-
To build the user guide, additionally validating external URLs, run:
|
354
|
-
|
355
|
-
```bash
|
356
|
-
(.venv) $ nox -s docs_check_urls
|
357
|
-
```
|
358
|
-
|
359
|
-
To build the user guide in a format suitable for viewing directly from the file system, run:
|
360
|
-
|
361
|
-
```bash
|
362
|
-
(.venv) $ nox -s docs_offline
|
363
|
-
```
|
364
|
-
|
365
|
-
To build and serve the user guide with automatic rebuilding as you change the contents,
|
366
|
-
run:
|
367
|
-
|
368
|
-
```bash
|
369
|
-
(.venv) $ nox -s docs_serve
|
370
|
-
```
|
371
|
-
|
372
|
-
and open <http://127.0.0.1:8000> in a browser.
|
373
|
-
|
374
|
-
Each time the `main` Git branch is updated, the
|
375
|
-
[`.github/workflows/pages.yml`](.github/workflows/pages.yml) GitHub Action will
|
376
|
-
automatically build the user guide and publish it to [GitHub Pages](https://pages.github.com/).
|
377
|
-
This is configured in the `docs_github_pages` Nox session.
|
378
|
-
|
379
|
-
## Generating API Documentation
|
380
|
-
|
381
|
-
This project uses [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) plugin for
|
382
|
-
MkDocs, which renders
|
383
|
-
[Google-style docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html)
|
384
|
-
into an MkDocs project. Google-style docstrings provide a good mix of easy-to-read docstrings in
|
385
|
-
code as well as nicely-rendered output.
|
386
|
-
|
387
|
-
```python
|
388
|
-
"""Computes the factorial through a recursive algorithm.
|
389
|
-
|
390
|
-
Args:
|
391
|
-
n: A positive input value.
|
392
|
-
|
393
|
-
Raises:
|
394
|
-
InvalidFactorialError: If n is less than 0.
|
395
|
-
|
396
|
-
Returns:
|
397
|
-
Computed factorial.
|
398
|
-
"""
|
399
|
-
```
|
400
|
-
|
401
|
-
## Misc
|
402
|
-
|
403
|
-
If you get a `Failed to create the collection: Prompt dismissed..` error when running `poetry update` on Ubuntu, try setting the following environment variable:
|
404
|
-
|
405
|
-
```bash
|
406
|
-
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
|
407
|
-
```
|
408
|
-
|
409
312
|
# Attributions
|
410
313
|
[python-blueprint](https://github.com/johnthagen/python-blueprint) for the Python package skeleton.
|
411
|
-
|
@@ -1,29 +1,30 @@
|
|
1
1
|
not_again_ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
not_again_ai/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93
|
2
3
|
not_again_ai/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
4
|
not_again_ai/base/file_system.py,sha256=KNQmacO4Q__CQuq2oPzWrg3rQO48n3evglc9bNiP7KM,949
|
4
5
|
not_again_ai/base/parallel.py,sha256=fcYhKBYBWvob84iKp3O93wvFFdXeidljZsShgBLTNGA,3448
|
5
6
|
not_again_ai/data/__init__.py,sha256=1jF6mwvtB2PT7IEc3xpbRtZm3g3Lyf8zUqH4AEE4qlQ,244
|
6
7
|
not_again_ai/data/web.py,sha256=wjx9cc33jcoJBGonYCIpwygPBFOwz7F-dx_ominmbnI,1838
|
7
|
-
not_again_ai/llm/__init__.py,sha256=
|
8
|
-
not_again_ai/llm/chat_completion/__init__.py,sha256=
|
9
|
-
not_again_ai/llm/chat_completion/interface.py,sha256=
|
8
|
+
not_again_ai/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
not_again_ai/llm/chat_completion/__init__.py,sha256=HozawvdRkTFgq8XR16GJUHN1ukEa4Ya68wVPVrl-afs,250
|
10
|
+
not_again_ai/llm/chat_completion/interface.py,sha256=xRZXQ75dxrkt5WNtOTtrAa2Oy4ZB-PG2WihW9FBmW-s,2525
|
11
|
+
not_again_ai/llm/chat_completion/types.py,sha256=Z_pQjVK_7rEvAE2fj5srKxHPFJBLPV8e0iCFibnzT7M,5596
|
10
12
|
not_again_ai/llm/chat_completion/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
not_again_ai/llm/chat_completion/providers/
|
12
|
-
not_again_ai/llm/chat_completion/providers/
|
13
|
-
not_again_ai/llm/chat_completion/
|
13
|
+
not_again_ai/llm/chat_completion/providers/anthropic_api.py,sha256=_-NPc5pfhsRwwy-GYc1vAiyc0agmGLyo5_7-mcPEnBU,6189
|
14
|
+
not_again_ai/llm/chat_completion/providers/ollama_api.py,sha256=Puo2eE2VynvZOoqrUlNYtPgRGCRMVa8syc3TfBxS1hs,10661
|
15
|
+
not_again_ai/llm/chat_completion/providers/openai_api.py,sha256=1wdeV50KYX_KIf2uofsICKYoHVSvj4kTRpS1Vuw3NSQ,17887
|
14
16
|
not_again_ai/llm/embedding/__init__.py,sha256=wscUfROukvw0M0vYccfaVTdXV0P-eICAT5mqM0LaHHc,182
|
15
17
|
not_again_ai/llm/embedding/interface.py,sha256=Hj3UiktXEeCUeMwpIDtRkwBfKgaJSnJvclLNyjwUAtE,1144
|
18
|
+
not_again_ai/llm/embedding/types.py,sha256=J4FFLx35Aow2kOaafDReeY9cUNqhWMjaAk5gXkX7SVk,506
|
16
19
|
not_again_ai/llm/embedding/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
20
|
not_again_ai/llm/embedding/providers/ollama_api.py,sha256=m-OCis9WAUT2baGsGVPzejlive40eSNyO6tHmPh6joM,3201
|
18
21
|
not_again_ai/llm/embedding/providers/openai_api.py,sha256=JFFqbq0O5snIEnr9VESdp5xehikQBPbs7nwyE6acFsY,5441
|
19
|
-
not_again_ai/llm/embedding/types.py,sha256=J4FFLx35Aow2kOaafDReeY9cUNqhWMjaAk5gXkX7SVk,506
|
20
22
|
not_again_ai/llm/prompting/__init__.py,sha256=7YnHro1yH01FLGnao27WyqQDFjNYf9npE5UxoR9YrUU,84
|
21
|
-
not_again_ai/llm/prompting/compile_prompt.py,sha256=
|
23
|
+
not_again_ai/llm/prompting/compile_prompt.py,sha256=uBn655yTiQ325z1CUgnkU2k7ICIvaYRJOm50B7w2lSs,4683
|
22
24
|
not_again_ai/llm/prompting/interface.py,sha256=SMKYabmu3zTWbEDukU6aLU_JQ88apeBWWOF_qZ0s3ww,1783
|
25
|
+
not_again_ai/llm/prompting/types.py,sha256=xz70dnawL9rji7Zr1_mOekY-uUlvKJJf7k9nXJsOXc4,1219
|
23
26
|
not_again_ai/llm/prompting/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
27
|
not_again_ai/llm/prompting/providers/openai_tiktoken.py,sha256=8YrEiK3ZHyKVGiVsJ_Rd6eVdISIvcub7ooj-HB7Prsc,4536
|
25
|
-
not_again_ai/llm/prompting/types.py,sha256=xz70dnawL9rji7Zr1_mOekY-uUlvKJJf7k9nXJsOXc4,1219
|
26
|
-
not_again_ai/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93
|
27
28
|
not_again_ai/statistics/__init__.py,sha256=gA8r9JQFbFSN0ykrHy4G1IQgcky4f2eM5Oo24oVI5Ik,466
|
28
29
|
not_again_ai/statistics/dependence.py,sha256=4xaniMkLlTjdXcNVXdwepEAiZ-WaaGYfR9haJC1lU2Q,4434
|
29
30
|
not_again_ai/viz/__init__.py,sha256=MeaWae_QRbDEHJ4MWYoY1-Ad6S0FhSDaRhQncS2cpSc,447
|
@@ -32,7 +33,7 @@ not_again_ai/viz/distributions.py,sha256=OyWwJaNI6lMRm_iSrhq-CORLNvXfeuLSgDtVo3u
|
|
32
33
|
not_again_ai/viz/scatterplot.py,sha256=5CUOWeknbBOaZPeX9oPin5sBkRKEwk8qeFH45R-9LlY,2292
|
33
34
|
not_again_ai/viz/time_series.py,sha256=pOGZqXp_2nd6nKo-PUQNCtmMh__69jxQ6bQibTGLwZA,5212
|
34
35
|
not_again_ai/viz/utils.py,sha256=hN7gwxtBt3U6jQni2K8j5m5pCXpaJDoNzGhBBikEU28,238
|
35
|
-
not_again_ai-0.
|
36
|
-
not_again_ai-0.
|
37
|
-
not_again_ai-0.
|
38
|
-
not_again_ai-0.
|
36
|
+
not_again_ai-0.18.0.dist-info/METADATA,sha256=n41TWZaLvs_XPNbSEQojju9DI4nPhkjE055xX7ZJGjQ,12021
|
37
|
+
not_again_ai-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
38
|
+
not_again_ai-0.18.0.dist-info/licenses/LICENSE,sha256=btjOgNGpp-ux5xOo1Gx1MddxeWtT9sof3s3Nui29QfA,1071
|
39
|
+
not_again_ai-0.18.0.dist-info/RECORD,,
|
File without changes
|