chatlas 0.2.0__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of chatlas might be problematic. Click here for more details.
- chatlas/_anthropic.py +101 -1
- chatlas/_chat.py +198 -5
- chatlas/_google.py +53 -1
- chatlas/_ollama.py +8 -0
- chatlas/_openai.py +63 -3
- chatlas/_provider.py +17 -0
- chatlas/types/anthropic/_client.py +0 -8
- chatlas/types/anthropic/_submit.py +1 -2
- chatlas/types/openai/_client.py +1 -0
- chatlas/types/openai/_client_azure.py +1 -0
- chatlas/types/openai/_submit.py +8 -2
- {chatlas-0.2.0.dist-info → chatlas-0.3.0.dist-info}/METADATA +16 -5
- {chatlas-0.2.0.dist-info → chatlas-0.3.0.dist-info}/RECORD +14 -14
- {chatlas-0.2.0.dist-info → chatlas-0.3.0.dist-info}/WHEEL +1 -1
chatlas/_anthropic.py
CHANGED
|
@@ -20,7 +20,7 @@ from ._logging import log_model_default
|
|
|
20
20
|
from ._provider import Provider
|
|
21
21
|
from ._tokens import tokens_log
|
|
22
22
|
from ._tools import Tool, basemodel_to_param_schema
|
|
23
|
-
from ._turn import Turn, normalize_turns
|
|
23
|
+
from ._turn import Turn, normalize_turns, user_turn
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from anthropic.types import (
|
|
@@ -380,6 +380,59 @@ class AnthropicProvider(Provider[Message, RawMessageStreamEvent, Message]):
|
|
|
380
380
|
def value_turn(self, completion, has_data_model) -> Turn:
|
|
381
381
|
return self._as_turn(completion, has_data_model)
|
|
382
382
|
|
|
383
|
+
def token_count(
|
|
384
|
+
self,
|
|
385
|
+
*args: Content | str,
|
|
386
|
+
tools: dict[str, Tool],
|
|
387
|
+
data_model: Optional[type[BaseModel]],
|
|
388
|
+
) -> int:
|
|
389
|
+
kwargs = self._token_count_args(
|
|
390
|
+
*args,
|
|
391
|
+
tools=tools,
|
|
392
|
+
data_model=data_model,
|
|
393
|
+
)
|
|
394
|
+
res = self._client.messages.count_tokens(**kwargs)
|
|
395
|
+
return res.input_tokens
|
|
396
|
+
|
|
397
|
+
async def token_count_async(
|
|
398
|
+
self,
|
|
399
|
+
*args: Content | str,
|
|
400
|
+
tools: dict[str, Tool],
|
|
401
|
+
data_model: Optional[type[BaseModel]],
|
|
402
|
+
) -> int:
|
|
403
|
+
kwargs = self._token_count_args(
|
|
404
|
+
*args,
|
|
405
|
+
tools=tools,
|
|
406
|
+
data_model=data_model,
|
|
407
|
+
)
|
|
408
|
+
res = await self._async_client.messages.count_tokens(**kwargs)
|
|
409
|
+
return res.input_tokens
|
|
410
|
+
|
|
411
|
+
def _token_count_args(
|
|
412
|
+
self,
|
|
413
|
+
*args: Content | str,
|
|
414
|
+
tools: dict[str, Tool],
|
|
415
|
+
data_model: Optional[type[BaseModel]],
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
turn = user_turn(*args)
|
|
418
|
+
|
|
419
|
+
kwargs = self._chat_perform_args(
|
|
420
|
+
stream=False,
|
|
421
|
+
turns=[turn],
|
|
422
|
+
tools=tools,
|
|
423
|
+
data_model=data_model,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
args_to_keep = [
|
|
427
|
+
"messages",
|
|
428
|
+
"model",
|
|
429
|
+
"system",
|
|
430
|
+
"tools",
|
|
431
|
+
"tool_choice",
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
return {arg: kwargs[arg] for arg in args_to_keep if arg in kwargs}
|
|
435
|
+
|
|
383
436
|
def _as_message_params(self, turns: list[Turn]) -> list["MessageParam"]:
|
|
384
437
|
messages: list["MessageParam"] = []
|
|
385
438
|
for turn in turns:
|
|
@@ -575,6 +628,53 @@ def ChatBedrockAnthropic(
|
|
|
575
628
|
Additional arguments to pass to the `anthropic.AnthropicBedrock()`
|
|
576
629
|
client constructor.
|
|
577
630
|
|
|
631
|
+
Troubleshooting
|
|
632
|
+
---------------
|
|
633
|
+
|
|
634
|
+
If you encounter 400 or 403 errors when trying to use the model, keep the
|
|
635
|
+
following in mind:
|
|
636
|
+
|
|
637
|
+
::: {.callout-note}
|
|
638
|
+
#### Incorrect model name
|
|
639
|
+
|
|
640
|
+
If the model name is completely incorrect, you'll see an error like
|
|
641
|
+
`Error code: 400 - {'message': 'The provided model identifier is invalid.'}`
|
|
642
|
+
|
|
643
|
+
Make sure the model name is correct and active in the specified region.
|
|
644
|
+
:::
|
|
645
|
+
|
|
646
|
+
::: {.callout-note}
|
|
647
|
+
#### Models are region specific
|
|
648
|
+
|
|
649
|
+
If you encounter errors similar to `Error code: 403 - {'message': "You don't
|
|
650
|
+
have access to the model with the specified model ID."}`, make sure your
|
|
651
|
+
model is active in the relevant `aws_region`.
|
|
652
|
+
|
|
653
|
+
Keep in mind, if `aws_region` is not specified, and AWS_REGION is not set,
|
|
654
|
+
the region defaults to us-east-1, which may not match to your AWS config's
|
|
655
|
+
default region.
|
|
656
|
+
:::
|
|
657
|
+
|
|
658
|
+
::: {.callout-note}
|
|
659
|
+
#### Cross region inference ID
|
|
660
|
+
|
|
661
|
+
In some cases, even if you have the right model and the right region, you
|
|
662
|
+
may still encounter an error like `Error code: 400 - {'message':
|
|
663
|
+
'Invocation of model ID anthropic.claude-3-5-sonnet-20240620-v1:0 with
|
|
664
|
+
on-demand throughput isn't supported. Retry your request with the ID or ARN
|
|
665
|
+
of an inference profile that contains this model.'}`
|
|
666
|
+
|
|
667
|
+
In this case, you'll need to look up the 'cross region inference ID' for
|
|
668
|
+
your model. This might required opening your `aws-console` and navigating to
|
|
669
|
+
the 'Anthropic Bedrock' service page. From there, go to the 'cross region
|
|
670
|
+
inference' tab and copy the relevant ID.
|
|
671
|
+
|
|
672
|
+
For example, if the desired model ID is
|
|
673
|
+
`anthropic.claude-3-5-sonnet-20240620-v1:0`, the cross region ID might look
|
|
674
|
+
something like `us.anthropic.claude-3-5-sonnet-20240620-v1:0`.
|
|
675
|
+
:::
|
|
676
|
+
|
|
677
|
+
|
|
578
678
|
Returns
|
|
579
679
|
-------
|
|
580
680
|
Chat
|
chatlas/_chat.py
CHANGED
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
Optional,
|
|
17
17
|
Sequence,
|
|
18
18
|
TypeVar,
|
|
19
|
+
overload,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from pydantic import BaseModel
|
|
@@ -176,17 +177,209 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
|
|
|
176
177
|
if value is not None:
|
|
177
178
|
self._turns.insert(0, Turn("system", value))
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
@overload
|
|
181
|
+
def tokens(self) -> list[tuple[int, int] | None]: ...
|
|
182
|
+
|
|
183
|
+
@overload
|
|
184
|
+
def tokens(
|
|
185
|
+
self,
|
|
186
|
+
values: Literal["cumulative"],
|
|
187
|
+
) -> list[tuple[int, int] | None]: ...
|
|
188
|
+
|
|
189
|
+
@overload
|
|
190
|
+
def tokens(
|
|
191
|
+
self,
|
|
192
|
+
values: Literal["discrete"],
|
|
193
|
+
) -> list[int]: ...
|
|
194
|
+
|
|
195
|
+
def tokens(
|
|
196
|
+
self,
|
|
197
|
+
values: Literal["cumulative", "discrete"] = "discrete",
|
|
198
|
+
) -> list[int] | list[tuple[int, int] | None]:
|
|
180
199
|
"""
|
|
181
200
|
Get the tokens for each turn in the chat.
|
|
182
201
|
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
values
|
|
205
|
+
If "cumulative" (the default), the result can be summed to get the
|
|
206
|
+
chat's overall token usage (helpful for computing overall cost of
|
|
207
|
+
the chat). If "discrete", the result can be summed to get the number of
|
|
208
|
+
tokens the turns will cost to generate the next response (helpful
|
|
209
|
+
for estimating cost of the next response, or for determining if you
|
|
210
|
+
are about to exceed the token limit).
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
list[int]
|
|
215
|
+
A list of token counts for each (non-system) turn in the chat. The
|
|
216
|
+
1st turn includes the tokens count for the system prompt (if any).
|
|
217
|
+
|
|
218
|
+
Raises
|
|
219
|
+
------
|
|
220
|
+
ValueError
|
|
221
|
+
If the chat's turns (i.e., `.get_turns()`) are not in an expected
|
|
222
|
+
format. This may happen if the chat history is manually set (i.e.,
|
|
223
|
+
`.set_turns()`). In this case, you can inspect the "raw" token
|
|
224
|
+
values via the `.get_turns()` method (each turn has a `.tokens`
|
|
225
|
+
attribute).
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
turns = self.get_turns(include_system_prompt=False)
|
|
229
|
+
|
|
230
|
+
if values == "cumulative":
|
|
231
|
+
return [turn.tokens for turn in turns]
|
|
232
|
+
|
|
233
|
+
if len(turns) == 0:
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
err_info = (
|
|
237
|
+
"This can happen if the chat history is manually set (i.e., `.set_turns()`). "
|
|
238
|
+
"Consider getting the 'raw' token values via the `.get_turns()` method "
|
|
239
|
+
"(each turn has a `.tokens` attribute)."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Sanity checks for the assumptions made to figure out user token counts
|
|
243
|
+
if len(turns) == 1:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"Expected at least two turns in the chat history. " + err_info
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if len(turns) % 2 != 0:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"Expected an even number of turns in the chat history. " + err_info
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if turns[0].role != "user":
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"Expected the 1st non-system turn to have role='user'. " + err_info
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if turns[1].role != "assistant":
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Expected the 2nd turn non-system to have role='assistant'. " + err_info
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if turns[1].tokens is None:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
"Expected the 1st assistant turn to contain token counts. " + err_info
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
res: list[int] = [
|
|
269
|
+
# Implied token count for the 1st user input
|
|
270
|
+
turns[1].tokens[0],
|
|
271
|
+
# The token count for the 1st assistant response
|
|
272
|
+
turns[1].tokens[1],
|
|
273
|
+
]
|
|
274
|
+
for i in range(1, len(turns) - 1, 2):
|
|
275
|
+
ti = turns[i]
|
|
276
|
+
tj = turns[i + 2]
|
|
277
|
+
if ti.role != "assistant" or tj.role != "assistant":
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"Expected even turns to have role='assistant'." + err_info
|
|
280
|
+
)
|
|
281
|
+
if ti.tokens is None or tj.tokens is None:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"Expected role='assistant' turns to contain token counts."
|
|
284
|
+
+ err_info
|
|
285
|
+
)
|
|
286
|
+
res.extend(
|
|
287
|
+
[
|
|
288
|
+
# Implied token count for the user input
|
|
289
|
+
tj.tokens[0] - sum(ti.tokens),
|
|
290
|
+
# The token count for the assistant response
|
|
291
|
+
tj.tokens[1],
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return res
|
|
296
|
+
|
|
297
|
+
def token_count(
|
|
298
|
+
self,
|
|
299
|
+
*args: Content | str,
|
|
300
|
+
data_model: Optional[type[BaseModel]] = None,
|
|
301
|
+
) -> int:
|
|
302
|
+
"""
|
|
303
|
+
Get an estimated token count for the given input.
|
|
304
|
+
|
|
305
|
+
Estimate the token size of input content. This can help determine whether input(s)
|
|
306
|
+
and/or conversation history (i.e., `.get_turns()`) should be reduced in size before
|
|
307
|
+
sending it to the model.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
args
|
|
312
|
+
The input to get a token count for.
|
|
313
|
+
data_model
|
|
314
|
+
If the input is meant for data extraction (i.e., `.extract_data()`), then
|
|
315
|
+
this should be the Pydantic model that describes the structure of the data to
|
|
316
|
+
extract.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
int
|
|
321
|
+
The token count for the input.
|
|
322
|
+
|
|
323
|
+
Note
|
|
324
|
+
----
|
|
325
|
+
Remember that the token count is an estimate. Also, models based on
|
|
326
|
+
`ChatOpenAI()` currently does not take tools into account when
|
|
327
|
+
estimating token counts.
|
|
328
|
+
|
|
329
|
+
Examples
|
|
330
|
+
--------
|
|
331
|
+
```python
|
|
332
|
+
from chatlas import ChatAnthropic
|
|
333
|
+
|
|
334
|
+
chat = ChatAnthropic()
|
|
335
|
+
# Estimate the token count before sending the input
|
|
336
|
+
print(chat.token_count("What is 2 + 2?"))
|
|
337
|
+
|
|
338
|
+
# Once input is sent, you can get the actual input and output
|
|
339
|
+
# token counts from the chat object
|
|
340
|
+
chat.chat("What is 2 + 2?", echo="none")
|
|
341
|
+
print(chat.token_usage())
|
|
342
|
+
```
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
return self.provider.token_count(
|
|
346
|
+
*args,
|
|
347
|
+
tools=self._tools,
|
|
348
|
+
data_model=data_model,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
async def token_count_async(
|
|
352
|
+
self,
|
|
353
|
+
*args: Content | str,
|
|
354
|
+
data_model: Optional[type[BaseModel]] = None,
|
|
355
|
+
) -> int:
|
|
356
|
+
"""
|
|
357
|
+
Get an estimated token count for the given input asynchronously.
|
|
358
|
+
|
|
359
|
+
Estimate the token size of input content. This can help determine whether input(s)
|
|
360
|
+
and/or conversation history (i.e., `.get_turns()`) should be reduced in size before
|
|
361
|
+
sending it to the model.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
args
|
|
366
|
+
The input to get a token count for.
|
|
367
|
+
data_model
|
|
368
|
+
If this input is meant for data extraction (i.e., `.extract_data_async()`),
|
|
369
|
+
then this should be the Pydantic model that describes the structure of the data
|
|
370
|
+
to extract.
|
|
371
|
+
|
|
183
372
|
Returns
|
|
184
373
|
-------
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
indices for a turn.
|
|
374
|
+
int
|
|
375
|
+
The token count for the input.
|
|
188
376
|
"""
|
|
189
|
-
|
|
377
|
+
|
|
378
|
+
return await self.provider.token_count_async(
|
|
379
|
+
*args,
|
|
380
|
+
tools=self._tools,
|
|
381
|
+
data_model=data_model,
|
|
382
|
+
)
|
|
190
383
|
|
|
191
384
|
def app(
|
|
192
385
|
self,
|
chatlas/_google.py
CHANGED
|
@@ -17,8 +17,9 @@ from ._content import (
|
|
|
17
17
|
)
|
|
18
18
|
from ._logging import log_model_default
|
|
19
19
|
from ._provider import Provider
|
|
20
|
+
from ._tokens import tokens_log
|
|
20
21
|
from ._tools import Tool, basemodel_to_param_schema
|
|
21
|
-
from ._turn import Turn, normalize_turns
|
|
22
|
+
from ._turn import Turn, normalize_turns, user_turn
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
25
|
from google.generativeai.types.content_types import (
|
|
@@ -332,6 +333,55 @@ class GoogleProvider(
|
|
|
332
333
|
def value_turn(self, completion, has_data_model) -> Turn:
|
|
333
334
|
return self._as_turn(completion, has_data_model)
|
|
334
335
|
|
|
336
|
+
def token_count(
|
|
337
|
+
self,
|
|
338
|
+
*args: Content | str,
|
|
339
|
+
tools: dict[str, Tool],
|
|
340
|
+
data_model: Optional[type[BaseModel]],
|
|
341
|
+
):
|
|
342
|
+
kwargs = self._token_count_args(
|
|
343
|
+
*args,
|
|
344
|
+
tools=tools,
|
|
345
|
+
data_model=data_model,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
res = self._client.count_tokens(**kwargs)
|
|
349
|
+
return res.total_tokens
|
|
350
|
+
|
|
351
|
+
async def token_count_async(
|
|
352
|
+
self,
|
|
353
|
+
*args: Content | str,
|
|
354
|
+
tools: dict[str, Tool],
|
|
355
|
+
data_model: Optional[type[BaseModel]],
|
|
356
|
+
):
|
|
357
|
+
kwargs = self._token_count_args(
|
|
358
|
+
*args,
|
|
359
|
+
tools=tools,
|
|
360
|
+
data_model=data_model,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
res = await self._client.count_tokens_async(**kwargs)
|
|
364
|
+
return res.total_tokens
|
|
365
|
+
|
|
366
|
+
def _token_count_args(
|
|
367
|
+
self,
|
|
368
|
+
*args: Content | str,
|
|
369
|
+
tools: dict[str, Tool],
|
|
370
|
+
data_model: Optional[type[BaseModel]],
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
turn = user_turn(*args)
|
|
373
|
+
|
|
374
|
+
kwargs = self._chat_perform_args(
|
|
375
|
+
stream=False,
|
|
376
|
+
turns=[turn],
|
|
377
|
+
tools=tools,
|
|
378
|
+
data_model=data_model,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
args_to_keep = ["contents", "tools"]
|
|
382
|
+
|
|
383
|
+
return {arg: kwargs[arg] for arg in args_to_keep if arg in kwargs}
|
|
384
|
+
|
|
335
385
|
def _google_contents(self, turns: list[Turn]) -> list["ContentDict"]:
|
|
336
386
|
contents: list["ContentDict"] = []
|
|
337
387
|
for turn in turns:
|
|
@@ -421,6 +471,8 @@ class GoogleProvider(
|
|
|
421
471
|
usage.candidates_token_count,
|
|
422
472
|
)
|
|
423
473
|
|
|
474
|
+
tokens_log(self, tokens)
|
|
475
|
+
|
|
424
476
|
finish = message.candidates[0].finish_reason
|
|
425
477
|
|
|
426
478
|
return Turn(
|
chatlas/_ollama.py
CHANGED
|
@@ -48,6 +48,13 @@ def ChatOllama(
|
|
|
48
48
|
(e.g. `ollama pull llama3.2`).
|
|
49
49
|
:::
|
|
50
50
|
|
|
51
|
+
::: {.callout-note}
|
|
52
|
+
## Python requirements
|
|
53
|
+
|
|
54
|
+
`ChatOllama` requires the `openai` package (e.g., `pip install openai`).
|
|
55
|
+
:::
|
|
56
|
+
|
|
57
|
+
|
|
51
58
|
Examples
|
|
52
59
|
--------
|
|
53
60
|
|
|
@@ -103,6 +110,7 @@ def ChatOllama(
|
|
|
103
110
|
|
|
104
111
|
return ChatOpenAI(
|
|
105
112
|
system_prompt=system_prompt,
|
|
113
|
+
api_key="ollama", # ignored
|
|
106
114
|
turns=turns,
|
|
107
115
|
base_url=f"{base_url}/v1",
|
|
108
116
|
model=model,
|
chatlas/_openai.py
CHANGED
|
@@ -8,6 +8,7 @@ from pydantic import BaseModel
|
|
|
8
8
|
from ._chat import Chat
|
|
9
9
|
from ._content import (
|
|
10
10
|
Content,
|
|
11
|
+
ContentImage,
|
|
11
12
|
ContentImageInline,
|
|
12
13
|
ContentImageRemote,
|
|
13
14
|
ContentJson,
|
|
@@ -20,7 +21,7 @@ from ._merge import merge_dicts
|
|
|
20
21
|
from ._provider import Provider
|
|
21
22
|
from ._tokens import tokens_log
|
|
22
23
|
from ._tools import Tool, basemodel_to_param_schema
|
|
23
|
-
from ._turn import Turn, normalize_turns
|
|
24
|
+
from ._turn import Turn, normalize_turns, user_turn
|
|
24
25
|
from ._utils import MISSING, MISSING_TYPE, is_testing
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
@@ -294,10 +295,12 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
294
295
|
"stream": stream,
|
|
295
296
|
"messages": self._as_message_param(turns),
|
|
296
297
|
"model": self._model,
|
|
297
|
-
"seed": self._seed,
|
|
298
298
|
**(kwargs or {}),
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
if self._seed is not None:
|
|
302
|
+
kwargs_full["seed"] = self._seed
|
|
303
|
+
|
|
301
304
|
if tool_schemas:
|
|
302
305
|
kwargs_full["tools"] = tool_schemas
|
|
303
306
|
|
|
@@ -349,6 +352,57 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
349
352
|
def value_turn(self, completion, has_data_model) -> Turn:
|
|
350
353
|
return self._as_turn(completion, has_data_model)
|
|
351
354
|
|
|
355
|
+
def token_count(
|
|
356
|
+
self,
|
|
357
|
+
*args: Content | str,
|
|
358
|
+
tools: dict[str, Tool],
|
|
359
|
+
data_model: Optional[type[BaseModel]],
|
|
360
|
+
) -> int:
|
|
361
|
+
try:
|
|
362
|
+
import tiktoken
|
|
363
|
+
except ImportError:
|
|
364
|
+
raise ImportError(
|
|
365
|
+
"The tiktoken package is required for token counting. "
|
|
366
|
+
"Please install it with `pip install tiktoken`."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
encoding = tiktoken.encoding_for_model(self._model)
|
|
370
|
+
|
|
371
|
+
turn = user_turn(*args)
|
|
372
|
+
|
|
373
|
+
# Count the tokens in image contents
|
|
374
|
+
image_tokens = sum(
|
|
375
|
+
self._image_token_count(x)
|
|
376
|
+
for x in turn.contents
|
|
377
|
+
if isinstance(x, ContentImage)
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# For other contents, get the token count from the actual message param
|
|
381
|
+
other_contents = [x for x in turn.contents if not isinstance(x, ContentImage)]
|
|
382
|
+
other_full = self._as_message_param([Turn("user", other_contents)])
|
|
383
|
+
other_tokens = len(encoding.encode(str(other_full)))
|
|
384
|
+
|
|
385
|
+
return other_tokens + image_tokens
|
|
386
|
+
|
|
387
|
+
async def token_count_async(
|
|
388
|
+
self,
|
|
389
|
+
*args: Content | str,
|
|
390
|
+
tools: dict[str, Tool],
|
|
391
|
+
data_model: Optional[type[BaseModel]],
|
|
392
|
+
) -> int:
|
|
393
|
+
return self.token_count(*args, tools=tools, data_model=data_model)
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _image_token_count(image: ContentImage) -> int:
|
|
397
|
+
if isinstance(image, ContentImageRemote) and image.detail == "low":
|
|
398
|
+
return 85
|
|
399
|
+
else:
|
|
400
|
+
# This is just the max token count for an image The highest possible
|
|
401
|
+
# resolution is 768 x 2048, and 8 tiles of size 512px can fit inside
|
|
402
|
+
# TODO: this is obviously a very conservative estimate and could be improved
|
|
403
|
+
# https://platform.openai.com/docs/guides/vision/calculating-costs
|
|
404
|
+
return 170 * 8 + 85
|
|
405
|
+
|
|
352
406
|
@staticmethod
|
|
353
407
|
def _as_message_param(turns: list[Turn]) -> list["ChatCompletionMessageParam"]:
|
|
354
408
|
from openai.types.chat import (
|
|
@@ -412,7 +466,13 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
412
466
|
contents.append({"type": "text", "text": ""})
|
|
413
467
|
elif isinstance(x, ContentImageRemote):
|
|
414
468
|
contents.append(
|
|
415
|
-
{
|
|
469
|
+
{
|
|
470
|
+
"type": "image_url",
|
|
471
|
+
"image_url": {
|
|
472
|
+
"url": x.url,
|
|
473
|
+
"detail": x.detail,
|
|
474
|
+
},
|
|
475
|
+
}
|
|
416
476
|
)
|
|
417
477
|
elif isinstance(x, ContentImageInline):
|
|
418
478
|
contents.append(
|
chatlas/_provider.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import (
|
|
|
14
14
|
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
|
|
17
|
+
from ._content import Content
|
|
17
18
|
from ._tools import Tool
|
|
18
19
|
from ._turn import Turn
|
|
19
20
|
|
|
@@ -141,3 +142,19 @@ class Provider(
|
|
|
141
142
|
completion: ChatCompletionT,
|
|
142
143
|
has_data_model: bool,
|
|
143
144
|
) -> Turn: ...
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def token_count(
|
|
148
|
+
self,
|
|
149
|
+
*args: Content | str,
|
|
150
|
+
tools: dict[str, Tool],
|
|
151
|
+
data_model: Optional[type[BaseModel]],
|
|
152
|
+
) -> int: ...
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
async def token_count_async(
|
|
156
|
+
self,
|
|
157
|
+
*args: Content | str,
|
|
158
|
+
tools: dict[str, Tool],
|
|
159
|
+
data_model: Optional[type[BaseModel]],
|
|
160
|
+
) -> int: ...
|
|
@@ -18,12 +18,4 @@ class ChatClientArgs(TypedDict, total=False):
|
|
|
18
18
|
default_headers: Optional[Mapping[str, str]]
|
|
19
19
|
default_query: Optional[Mapping[str, object]]
|
|
20
20
|
http_client: httpx.AsyncClient
|
|
21
|
-
transport: httpx.AsyncBaseTransport
|
|
22
|
-
proxies: Union[
|
|
23
|
-
str,
|
|
24
|
-
httpx.Proxy,
|
|
25
|
-
dict[str | httpx.URL, Union[None, str, httpx.URL, httpx.Proxy]],
|
|
26
|
-
None,
|
|
27
|
-
]
|
|
28
|
-
connection_pool_limits: httpx.Limits
|
|
29
21
|
_strict_response_validation: bool
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
from typing import Iterable, Literal, Mapping, Optional, TypedDict, Union
|
|
7
7
|
|
|
8
8
|
import anthropic
|
|
9
|
-
import anthropic._types
|
|
10
9
|
import anthropic.types.message_param
|
|
11
10
|
import anthropic.types.text_block_param
|
|
12
11
|
import anthropic.types.tool_choice_any_param
|
|
@@ -51,7 +50,7 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
51
50
|
tools: Union[Iterable[anthropic.types.tool_param.ToolParam], anthropic.NotGiven]
|
|
52
51
|
top_k: int | anthropic.NotGiven
|
|
53
52
|
top_p: float | anthropic.NotGiven
|
|
54
|
-
extra_headers: Optional[Mapping[str, Union[str, anthropic.
|
|
53
|
+
extra_headers: Optional[Mapping[str, Union[str, anthropic.Omit]]]
|
|
55
54
|
extra_query: Optional[Mapping[str, object]]
|
|
56
55
|
extra_body: object | None
|
|
57
56
|
timeout: float | anthropic.Timeout | None | anthropic.NotGiven
|
chatlas/types/openai/_client.py
CHANGED
|
@@ -14,6 +14,7 @@ class ChatClientArgs(TypedDict, total=False):
|
|
|
14
14
|
organization: str | None
|
|
15
15
|
project: str | None
|
|
16
16
|
base_url: str | httpx.URL | None
|
|
17
|
+
websocket_base_url: str | httpx.URL | None
|
|
17
18
|
timeout: Union[float, openai.Timeout, None, openai.NotGiven]
|
|
18
19
|
max_retries: int
|
|
19
20
|
default_headers: Optional[Mapping[str, str]]
|
|
@@ -17,6 +17,7 @@ class ChatAzureClientArgs(TypedDict, total=False):
|
|
|
17
17
|
organization: str | None
|
|
18
18
|
project: str | None
|
|
19
19
|
base_url: str | None
|
|
20
|
+
websocket_base_url: str | httpx.URL | None
|
|
20
21
|
timeout: float | openai.Timeout | None | openai.NotGiven
|
|
21
22
|
max_retries: int
|
|
22
23
|
default_headers: Optional[Mapping[str, str]]
|
chatlas/types/openai/_submit.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Iterable, Literal, Mapping, Optional, TypedDict, Union
|
|
|
8
8
|
import openai
|
|
9
9
|
import openai.types.chat.chat_completion_assistant_message_param
|
|
10
10
|
import openai.types.chat.chat_completion_audio_param
|
|
11
|
+
import openai.types.chat.chat_completion_developer_message_param
|
|
11
12
|
import openai.types.chat.chat_completion_function_call_option_param
|
|
12
13
|
import openai.types.chat.chat_completion_function_message_param
|
|
13
14
|
import openai.types.chat.chat_completion_named_tool_choice_param
|
|
@@ -26,6 +27,7 @@ import openai.types.shared_params.response_format_text
|
|
|
26
27
|
class SubmitInputArgs(TypedDict, total=False):
|
|
27
28
|
messages: Iterable[
|
|
28
29
|
Union[
|
|
30
|
+
openai.types.chat.chat_completion_developer_message_param.ChatCompletionDeveloperMessageParam,
|
|
29
31
|
openai.types.chat.chat_completion_system_message_param.ChatCompletionSystemMessageParam,
|
|
30
32
|
openai.types.chat.chat_completion_user_message_param.ChatCompletionUserMessageParam,
|
|
31
33
|
openai.types.chat.chat_completion_assistant_message_param.ChatCompletionAssistantMessageParam,
|
|
@@ -36,6 +38,8 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
36
38
|
model: Union[
|
|
37
39
|
str,
|
|
38
40
|
Literal[
|
|
41
|
+
"o1",
|
|
42
|
+
"o1-2024-12-17",
|
|
39
43
|
"o1-preview",
|
|
40
44
|
"o1-preview-2024-09-12",
|
|
41
45
|
"o1-mini",
|
|
@@ -44,10 +48,11 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
44
48
|
"gpt-4o-2024-11-20",
|
|
45
49
|
"gpt-4o-2024-08-06",
|
|
46
50
|
"gpt-4o-2024-05-13",
|
|
47
|
-
"gpt-4o-realtime-preview",
|
|
48
|
-
"gpt-4o-realtime-preview-2024-10-01",
|
|
49
51
|
"gpt-4o-audio-preview",
|
|
50
52
|
"gpt-4o-audio-preview-2024-10-01",
|
|
53
|
+
"gpt-4o-audio-preview-2024-12-17",
|
|
54
|
+
"gpt-4o-mini-audio-preview",
|
|
55
|
+
"gpt-4o-mini-audio-preview-2024-12-17",
|
|
51
56
|
"chatgpt-4o-latest",
|
|
52
57
|
"gpt-4o-mini",
|
|
53
58
|
"gpt-4o-mini-2024-07-18",
|
|
@@ -100,6 +105,7 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
100
105
|
openai.NotGiven,
|
|
101
106
|
]
|
|
102
107
|
presence_penalty: Union[float, None, openai.NotGiven]
|
|
108
|
+
reasoning_effort: Union[Literal["low", "medium", "high"], openai.NotGiven]
|
|
103
109
|
response_format: Union[
|
|
104
110
|
openai.types.shared_params.response_format_text.ResponseFormatText,
|
|
105
111
|
openai.types.shared_params.response_format_json_object.ResponseFormatJSONObject,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: chatlas
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A simple and consistent interface for chatting with LLMs
|
|
5
5
|
Project-URL: Homepage, https://posit-dev.github.io/chatlas
|
|
6
6
|
Project-URL: Documentation, https://posit-dev.github.io/chatlas
|
|
@@ -30,15 +30,18 @@ Requires-Dist: pillow; extra == 'dev'
|
|
|
30
30
|
Requires-Dist: python-dotenv; extra == 'dev'
|
|
31
31
|
Requires-Dist: ruff>=0.6.5; extra == 'dev'
|
|
32
32
|
Requires-Dist: shiny; extra == 'dev'
|
|
33
|
+
Requires-Dist: tiktoken; extra == 'dev'
|
|
33
34
|
Provides-Extra: docs
|
|
34
35
|
Requires-Dist: griffe>=1; extra == 'docs'
|
|
35
36
|
Requires-Dist: ipykernel; extra == 'docs'
|
|
36
37
|
Requires-Dist: ipywidgets; extra == 'docs'
|
|
37
38
|
Requires-Dist: nbclient; extra == 'docs'
|
|
38
39
|
Requires-Dist: nbformat; extra == 'docs'
|
|
40
|
+
Requires-Dist: numpy; extra == 'docs'
|
|
39
41
|
Requires-Dist: pandas; extra == 'docs'
|
|
40
42
|
Requires-Dist: pyyaml; extra == 'docs'
|
|
41
43
|
Requires-Dist: quartodoc>=0.7; extra == 'docs'
|
|
44
|
+
Requires-Dist: sentence-transformers; extra == 'docs'
|
|
42
45
|
Provides-Extra: test
|
|
43
46
|
Requires-Dist: pyright>=1.1.379; extra == 'test'
|
|
44
47
|
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
@@ -48,6 +51,14 @@ Description-Content-Type: text/markdown
|
|
|
48
51
|
|
|
49
52
|
# chatlas
|
|
50
53
|
|
|
54
|
+
<p>
|
|
55
|
+
<!-- badges start -->
|
|
56
|
+
<a href="https://pypi.org/project/chatlas/"><img alt="PyPI" src="https://img.shields.io/pypi/v/chatlas?logo=python&logoColor=white&color=orange"></a>
|
|
57
|
+
<a href="https://choosealicense.com/licenses/mit/"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
|
|
58
|
+
<a href="https://github.com/posit-dev/chatlas"><img src="https://github.com/posit-dev/chatlas/actions/workflows/test.yml/badge.svg?branch=main" alt="Python Tests"></a>
|
|
59
|
+
<!-- badges end -->
|
|
60
|
+
</p>
|
|
61
|
+
|
|
51
62
|
chatlas provides a simple and unified interface across large language model (llm) providers in Python.
|
|
52
63
|
It abstracts away complexity from common tasks like streaming chat interfaces, tool calling, structured output, and much more.
|
|
53
64
|
chatlas helps you prototype faster without painting you into a corner; for example, switching providers is as easy as changing one line of code, but provider specific features are still accessible when needed.
|
|
@@ -123,7 +134,7 @@ From a `chat` instance, it's simple to start a web-based or terminal-based chat
|
|
|
123
134
|
chat.app()
|
|
124
135
|
```
|
|
125
136
|
|
|
126
|
-
<div
|
|
137
|
+
<div align="center">
|
|
127
138
|
<img width="500" alt="A web app for chatting with an LLM via chatlas" src="https://github.com/user-attachments/assets/e43f60cb-3686-435a-bd11-8215cb024d2e" class="border rounded">
|
|
128
139
|
</div>
|
|
129
140
|
|
|
@@ -279,7 +290,7 @@ asyncio.run(main())
|
|
|
279
290
|
|
|
280
291
|
`chatlas` has full typing support, meaning that, among other things, autocompletion just works in your favorite editor:
|
|
281
292
|
|
|
282
|
-
<div
|
|
293
|
+
<div align="center">
|
|
283
294
|
<img width="500" alt="Autocompleting model options in ChatOpenAI" src="https://github.com/user-attachments/assets/163d6d8a-7d58-422d-b3af-cc9f2adee759" class="rounded">
|
|
284
295
|
</div>
|
|
285
296
|
|
|
@@ -299,7 +310,7 @@ This shows important information like tool call results, finish reasons, and mor
|
|
|
299
310
|
If the problem isn't self-evident, you can also reach into the `.get_last_turn()`, which contains the full response object, with full details about the completion.
|
|
300
311
|
|
|
301
312
|
|
|
302
|
-
<div
|
|
313
|
+
<div align="center">
|
|
303
314
|
<img width="500" alt="Turn completion details with typing support" src="https://github.com/user-attachments/assets/eaea338d-e44a-4e23-84a7-2e998d8af3ba" class="rounded">
|
|
304
315
|
</div>
|
|
305
316
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
chatlas/__init__.py,sha256=OJbTO71ne1O9SDxkwIKOMpCMKbh0T8eDpYPFhrAb28A,974
|
|
2
|
-
chatlas/_anthropic.py,sha256=
|
|
3
|
-
chatlas/_chat.py,sha256=
|
|
2
|
+
chatlas/_anthropic.py,sha256=ssv6k-XZetwv1hNk7Qs8dAWlcPvElzxjrG0Fh2w50xE,24373
|
|
3
|
+
chatlas/_chat.py,sha256=p_MS1LQ-EBlvCzGTbjY5Bdlt1meDsfSbP517blkwSjY,44774
|
|
4
4
|
chatlas/_content.py,sha256=vpWF_WKS2tCDUtnL8l9lfW6b6g9e7LbDKP-_TegauVE,5883
|
|
5
5
|
chatlas/_content_image.py,sha256=4nk9wTvLtNmtcytdFp8p9otEV5-0_K6wzIxCyK0PIEI,8367
|
|
6
6
|
chatlas/_display.py,sha256=_IcQcvpyTNjGHOpY70_LOrDWwTjzdkziy6pTvxHEiWI,4053
|
|
7
7
|
chatlas/_github.py,sha256=D3L7Qu35K-M1qEW7-w-Oq-pF-9mVetia3MHYNNLEYtU,4373
|
|
8
|
-
chatlas/_google.py,sha256=
|
|
8
|
+
chatlas/_google.py,sha256=jfBp_C_qjbvs48QPE_ykSsfRhjz7htb-G389qLxvSC4,15279
|
|
9
9
|
chatlas/_groq.py,sha256=3VnYiKdxJTHPhEgUKnL2nY5uYL2L4PKBo7GZMwR0D8k,4158
|
|
10
10
|
chatlas/_interpolate.py,sha256=ykwLP3x-ya9Q33U4knSU75dtk6pzJAeythEEIW-43Pc,3631
|
|
11
11
|
chatlas/_logging.py,sha256=7a20sAl1PkW1qBNrfd_ieUbQXV8Gf4Vuf0Wn62LNBmk,2290
|
|
12
12
|
chatlas/_merge.py,sha256=Xt2uutLdEmYAGfGCa8GCEd8sdNadQM5o3l-zuIQFbWU,3923
|
|
13
|
-
chatlas/_ollama.py,sha256=
|
|
14
|
-
chatlas/_openai.py,sha256=
|
|
13
|
+
chatlas/_ollama.py,sha256=ze-RoHEbf62dYmXDDKjNGqaEZaKCZdcBEyFwQMDQxkQ,3760
|
|
14
|
+
chatlas/_openai.py,sha256=nJgp3JJlpxeXn_dhIrXNffQk9vh3ycYy1D2fNip0qEs,24004
|
|
15
15
|
chatlas/_perplexity.py,sha256=Bw_mlM8N8egGKIrbNerTn2pMlybugADOshjYOfN1ixM,4446
|
|
16
|
-
chatlas/_provider.py,sha256=
|
|
16
|
+
chatlas/_provider.py,sha256=i16I2hkBat1fYEMcFsU0gYNr6Tcg8zJ2xHDnEY7WRY4,4009
|
|
17
17
|
chatlas/_tokens.py,sha256=3W3EPUp9eWXUiwuzJwEPBv43AUznbK46pm59Htti7z4,2392
|
|
18
18
|
chatlas/_tokens_old.py,sha256=L9d9oafrXvEx2u4nIn_Jjn7adnQyLBnYBuPwJUE8Pl8,5005
|
|
19
19
|
chatlas/_tools.py,sha256=-qt4U1AFkebQoX9kpsBy5QXK8a2PpHX6Amgm44gcQ68,4113
|
|
@@ -22,16 +22,16 @@ chatlas/_typing_extensions.py,sha256=YdzmlyPSBpIEcsOkoz12e6jETT1XEMV2Q72haE4cfwY
|
|
|
22
22
|
chatlas/_utils.py,sha256=qAiWuDx-uG8BGFZ_PWvum9wpN-WogdItO32X4pRhhLs,2762
|
|
23
23
|
chatlas/types/__init__.py,sha256=pgHl8pd2Ytskd6lkfNtm98Yj1ZP0b3R35RH4Uht2BAs,694
|
|
24
24
|
chatlas/types/anthropic/__init__.py,sha256=OwubA-DPHYpYo0XyRyAFwftOI0mOxtHzAyhUSLcDx54,417
|
|
25
|
-
chatlas/types/anthropic/_client.py,sha256=
|
|
25
|
+
chatlas/types/anthropic/_client.py,sha256=G0LRhoFBcsSOMr5qhP-0rAScsVXaVlHCpggfVp54bnQ,690
|
|
26
26
|
chatlas/types/anthropic/_client_bedrock.py,sha256=mNazQlu0pQt8JdzrYn3LKNgE4n732GjhQUJdQQK9QkY,785
|
|
27
|
-
chatlas/types/anthropic/_submit.py,sha256=
|
|
27
|
+
chatlas/types/anthropic/_submit.py,sha256=BhkJu0OhIMiD3qiUNqJ4litBQCW-7KRr_YZp9Xr9KGA,2091
|
|
28
28
|
chatlas/types/google/__init__.py,sha256=ZJhi8Kwvio2zp8T1TQqmvdHqkS-Khb6BGESPjREADgo,337
|
|
29
29
|
chatlas/types/google/_client.py,sha256=YA5hsT-m-KcONKtwpCULYMnGwMPfkScpvhjx_qBLg5o,4421
|
|
30
30
|
chatlas/types/google/_submit.py,sha256=yp1wtp5eScLlHDNxeXl0qJOKv7SWLnRQ8oslupRFUBE,4839
|
|
31
31
|
chatlas/types/openai/__init__.py,sha256=Q2RAr1bSH1nHsxICK05nAmKmxdhKmhbBkWD_XHiVSrI,411
|
|
32
|
-
chatlas/types/openai/_client.py,sha256=
|
|
33
|
-
chatlas/types/openai/_client_azure.py,sha256=
|
|
34
|
-
chatlas/types/openai/_submit.py,sha256=
|
|
35
|
-
chatlas-0.
|
|
36
|
-
chatlas-0.
|
|
37
|
-
chatlas-0.
|
|
32
|
+
chatlas/types/openai/_client.py,sha256=YGm_EHtRSSHeeOZe-CV7oNvMJpEblEta3UTuU7lSRO8,754
|
|
33
|
+
chatlas/types/openai/_client_azure.py,sha256=jx8D_p46CLDGzTP-k-TtGzj-f3junj6or-86m8DD_0w,858
|
|
34
|
+
chatlas/types/openai/_submit.py,sha256=f2o3rNJZcWBhwjomXrs3Mh0V_4vNa9N5eEBkMvJDofQ,6028
|
|
35
|
+
chatlas-0.3.0.dist-info/METADATA,sha256=eIPyvdcXpYN4s6txY5GMgBttUsmS5868D10U5zzTchs,13164
|
|
36
|
+
chatlas-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
37
|
+
chatlas-0.3.0.dist-info/RECORD,,
|