chatlas 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of chatlas might be problematic. Click here for more details.

Files changed (152) hide show
  1. {chatlas-0.2.0 → chatlas-0.3.0}/.github/workflows/release.yml +1 -4
  2. chatlas-0.3.0/CHANGELOG.md +26 -0
  3. {chatlas-0.2.0 → chatlas-0.3.0}/PKG-INFO +16 -5
  4. {chatlas-0.2.0 → chatlas-0.3.0}/README.md +11 -3
  5. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_anthropic.py +101 -1
  6. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_chat.py +198 -5
  7. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_google.py +53 -1
  8. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_ollama.py +8 -0
  9. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_openai.py +63 -3
  10. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_provider.py +17 -0
  11. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/anthropic/_client.py +0 -8
  12. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/anthropic/_submit.py +1 -2
  13. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/openai/_client.py +1 -0
  14. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/openai/_client_azure.py +1 -0
  15. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/openai/_submit.py +8 -2
  16. {chatlas-0.2.0 → chatlas-0.3.0}/docs/_quarto.yml +2 -0
  17. {chatlas-0.2.0 → chatlas-0.3.0}/docs/get-started.qmd +1 -1
  18. {chatlas-0.2.0 → chatlas-0.3.0}/docs/prompt-design.qmd +10 -10
  19. chatlas-0.3.0/docs/rag.qmd +78 -0
  20. {chatlas-0.2.0 → chatlas-0.3.0}/docs/web-apps.qmd +6 -6
  21. {chatlas-0.2.0 → chatlas-0.3.0}/pyproject.toml +3 -0
  22. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_provider_openai.py +1 -1
  23. chatlas-0.3.0/tests/test_tokens.py +72 -0
  24. chatlas-0.2.0/CHANGELOG.md +0 -12
  25. chatlas-0.2.0/tests/test_tokens.py +0 -34
  26. {chatlas-0.2.0 → chatlas-0.3.0}/.github/workflows/check-update-types.yml +0 -0
  27. {chatlas-0.2.0 → chatlas-0.3.0}/.github/workflows/docs-publish.yml +0 -0
  28. {chatlas-0.2.0 → chatlas-0.3.0}/.github/workflows/test.yml +0 -0
  29. {chatlas-0.2.0 → chatlas-0.3.0}/.gitignore +0 -0
  30. {chatlas-0.2.0 → chatlas-0.3.0}/.vscode/extensions.json +0 -0
  31. {chatlas-0.2.0 → chatlas-0.3.0}/.vscode/settings.json +0 -0
  32. {chatlas-0.2.0 → chatlas-0.3.0}/Makefile +0 -0
  33. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/__init__.py +0 -0
  34. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_content.py +0 -0
  35. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_content_image.py +0 -0
  36. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_display.py +0 -0
  37. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_github.py +0 -0
  38. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_groq.py +0 -0
  39. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_interpolate.py +0 -0
  40. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_logging.py +0 -0
  41. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_merge.py +0 -0
  42. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_perplexity.py +0 -0
  43. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_tokens.py +0 -0
  44. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_tokens_old.py +0 -0
  45. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_tools.py +0 -0
  46. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_turn.py +0 -0
  47. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_typing_extensions.py +0 -0
  48. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/_utils.py +0 -0
  49. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/__init__.py +0 -0
  50. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/anthropic/__init__.py +0 -0
  51. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/anthropic/_client_bedrock.py +0 -0
  52. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/google/__init__.py +0 -0
  53. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/google/_client.py +0 -0
  54. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/google/_submit.py +0 -0
  55. {chatlas-0.2.0 → chatlas-0.3.0}/chatlas/types/openai/__init__.py +0 -0
  56. {chatlas-0.2.0 → chatlas-0.3.0}/docs/.gitignore +0 -0
  57. {chatlas-0.2.0 → chatlas-0.3.0}/docs/_extensions/machow/interlinks/.gitignore +0 -0
  58. {chatlas-0.2.0 → chatlas-0.3.0}/docs/_extensions/machow/interlinks/_extension.yml +0 -0
  59. {chatlas-0.2.0 → chatlas-0.3.0}/docs/_extensions/machow/interlinks/interlinks.lua +0 -0
  60. {chatlas-0.2.0 → chatlas-0.3.0}/docs/_sidebar.yml +0 -0
  61. {chatlas-0.2.0 → chatlas-0.3.0}/docs/congressional-assets.png +0 -0
  62. {chatlas-0.2.0 → chatlas-0.3.0}/docs/examples/third-party-testing.txt +0 -0
  63. {chatlas-0.2.0 → chatlas-0.3.0}/docs/images/congressional-assets.png +0 -0
  64. {chatlas-0.2.0 → chatlas-0.3.0}/docs/images/tool-calling-right.svg +0 -0
  65. {chatlas-0.2.0 → chatlas-0.3.0}/docs/images/tool-calling-wrong.svg +0 -0
  66. {chatlas-0.2.0 → chatlas-0.3.0}/docs/index.py +0 -0
  67. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/android-chrome-192x192.png +0 -0
  68. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/android-chrome-512x512.png +0 -0
  69. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/apple-touch-icon.png +0 -0
  70. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/favicon-16x16.png +0 -0
  71. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/favicon-32x32.png +0 -0
  72. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/favicon/favicon.ico +0 -0
  73. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-black.png +0 -0
  74. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-black.svg +0 -0
  75. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-color.png +0 -0
  76. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-color.svg +0 -0
  77. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-white.png +0 -0
  78. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/icon/brand-yml-icon-white.svg +0 -0
  79. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-black.png +0 -0
  80. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-black.svg +0 -0
  81. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-color.png +0 -0
  82. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-color.svg +0 -0
  83. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-white.png +0 -0
  84. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/tall/brand-yml-tall-white.svg +0 -0
  85. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-black.png +0 -0
  86. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-black.svg +0 -0
  87. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-color.png +0 -0
  88. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-color.svg +0 -0
  89. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-large-black.png +0 -0
  90. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-large-color.png +0 -0
  91. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-large-white.png +0 -0
  92. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-white.png +0 -0
  93. {chatlas-0.2.0 → chatlas-0.3.0}/docs/logos/wide/brand-yml-wide-white.svg +0 -0
  94. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/Chat.qmd +0 -0
  95. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatAnthropic.qmd +0 -0
  96. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatAzureOpenAI.qmd +0 -0
  97. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatBedrockAnthropic.qmd +0 -0
  98. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatGithub.qmd +0 -0
  99. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatGoogle.qmd +0 -0
  100. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatGroq.qmd +0 -0
  101. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatOllama.qmd +0 -0
  102. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatOpenAI.qmd +0 -0
  103. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/ChatPerplexity.qmd +0 -0
  104. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/Provider.qmd +0 -0
  105. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/Tool.qmd +0 -0
  106. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/Turn.qmd +0 -0
  107. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/content_image_file.qmd +0 -0
  108. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/content_image_plot.qmd +0 -0
  109. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/content_image_url.qmd +0 -0
  110. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/image_file.qmd +0 -0
  111. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/image_plot.qmd +0 -0
  112. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/image_url.qmd +0 -0
  113. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/index.qmd +0 -0
  114. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/token_usage.qmd +0 -0
  115. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ChatResponse.qmd +0 -0
  116. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ChatResponseAsync.qmd +0 -0
  117. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.Content.qmd +0 -0
  118. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentImage.qmd +0 -0
  119. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentImageInline.qmd +0 -0
  120. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentImageRemote.qmd +0 -0
  121. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentJson.qmd +0 -0
  122. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentText.qmd +0 -0
  123. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentToolRequest.qmd +0 -0
  124. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ContentToolResult.qmd +0 -0
  125. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.ImageContentTypes.qmd +0 -0
  126. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.MISSING.qmd +0 -0
  127. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.MISSING_TYPE.qmd +0 -0
  128. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.SubmitInputArgsT.qmd +0 -0
  129. {chatlas-0.2.0 → chatlas-0.3.0}/docs/reference/types.TokenUsage.qmd +0 -0
  130. {chatlas-0.2.0 → chatlas-0.3.0}/docs/structured-data.qmd +0 -0
  131. {chatlas-0.2.0 → chatlas-0.3.0}/docs/styles.scss +0 -0
  132. {chatlas-0.2.0 → chatlas-0.3.0}/docs/tool-calling.qmd +0 -0
  133. {chatlas-0.2.0 → chatlas-0.3.0}/pytest.ini +0 -0
  134. {chatlas-0.2.0 → chatlas-0.3.0}/scripts/_generate_anthropic_types.py +0 -0
  135. {chatlas-0.2.0 → chatlas-0.3.0}/scripts/_generate_google_types.py +0 -0
  136. {chatlas-0.2.0 → chatlas-0.3.0}/scripts/_generate_openai_types.py +0 -0
  137. {chatlas-0.2.0 → chatlas-0.3.0}/scripts/_utils.py +0 -0
  138. {chatlas-0.2.0 → chatlas-0.3.0}/scripts/main.py +0 -0
  139. {chatlas-0.2.0 → chatlas-0.3.0}/tests/__init__.py +0 -0
  140. {chatlas-0.2.0 → chatlas-0.3.0}/tests/__snapshots__/test_chat.ambr +0 -0
  141. {chatlas-0.2.0 → chatlas-0.3.0}/tests/conftest.py +0 -0
  142. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_chat.py +0 -0
  143. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_content.py +0 -0
  144. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_content_image.py +0 -0
  145. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_content_tools.py +0 -0
  146. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_interpolate.py +0 -0
  147. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_provider_anthropic.py +0 -0
  148. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_provider_azure.py +0 -0
  149. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_provider_bedrock.py +0 -0
  150. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_provider_google.py +0 -0
  151. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_turns.py +0 -0
  152. {chatlas-0.2.0 → chatlas-0.3.0}/tests/test_utils_merge.py +0 -0
@@ -5,8 +5,7 @@ on:
5
5
  types: [published]
6
6
 
7
7
  env:
8
- UV_VERSION: "0.4.x"
9
- PYTHON_VERSION: 3.13
8
+ PYTHON_VERSION: 3.12
10
9
 
11
10
  jobs:
12
11
  pypi-release:
@@ -27,8 +26,6 @@ jobs:
27
26
 
28
27
  - name: 🚀 Install uv
29
28
  uses: astral-sh/setup-uv@v3
30
- with:
31
- version: ${{ env.UV_VERSION }}
32
29
 
33
30
  - name: 🐍 Set up Python ${{ env.PYTHON_VERSION }}
34
31
  run: uv python install ${{ env.PYTHON_VERSION }}
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ <!--
4
+ All notable changes to this project will be documented in this file.
5
+
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+ -->
9
+
10
+ ## [0.3.0] - 2024-12-20
11
+
12
+ ### New features
13
+
14
+ * `Chat`'s `.tokens()` method gains a `values` argument. Set it to `"discrete"` to get a result that can be summed to determine the token cost of submitting the current turns. The default (`"cumulative"`), remains the same (the result can be summed to determine the overall token cost of the conversation).
15
+ * `Chat` gains a `.token_count()` method to help estimate token cost of new input. (#23)
16
+
17
+ ### Bug fixes
18
+
19
+ * `ChatOllama` no longer fails when a `OPENAI_API_KEY` environment variable is not set.
20
+ * `ChatOpenAI` now correctly includes the relevant `detail` on `ContentImageRemote()` input.
21
+ * `ChatGoogle` now correctly logs its `token_usage()`. (#23)
22
+
23
+
24
+ ## [0.2.0] - 2024-12-11
25
+
26
+ First stable release of `chatlas`, see the website to learn more <https://posit-dev.github.io/chatlas/>
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: chatlas
3
- Version: 0.2.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 style="display:flex;justify-content:center;">
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 style="display:flex;justify-content:center;">
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 style="display:flex;justify-content:center;">
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,5 +1,13 @@
1
1
  # chatlas
2
2
 
3
+ <p>
4
+ <!-- badges start -->
5
+ <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>
6
+ <a href="https://choosealicense.com/licenses/mit/"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
7
+ <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>
8
+ <!-- badges end -->
9
+ </p>
10
+
3
11
  chatlas provides a simple and unified interface across large language model (llm) providers in Python.
4
12
  It abstracts away complexity from common tasks like streaming chat interfaces, tool calling, structured output, and much more.
5
13
  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.
@@ -75,7 +83,7 @@ From a `chat` instance, it's simple to start a web-based or terminal-based chat
75
83
  chat.app()
76
84
  ```
77
85
 
78
- <div style="display:flex;justify-content:center;">
86
+ <div align="center">
79
87
  <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">
80
88
  </div>
81
89
 
@@ -231,7 +239,7 @@ asyncio.run(main())
231
239
 
232
240
  `chatlas` has full typing support, meaning that, among other things, autocompletion just works in your favorite editor:
233
241
 
234
- <div style="display:flex;justify-content:center;">
242
+ <div align="center">
235
243
  <img width="500" alt="Autocompleting model options in ChatOpenAI" src="https://github.com/user-attachments/assets/163d6d8a-7d58-422d-b3af-cc9f2adee759" class="rounded">
236
244
  </div>
237
245
 
@@ -251,7 +259,7 @@ This shows important information like tool call results, finish reasons, and mor
251
259
  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.
252
260
 
253
261
 
254
- <div style="display:flex;justify-content:center;">
262
+ <div align="center">
255
263
  <img width="500" alt="Turn completion details with typing support" src="https://github.com/user-attachments/assets/eaea338d-e44a-4e23-84a7-2e998d8af3ba" class="rounded">
256
264
  </div>
257
265
 
@@ -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
@@ -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
- def tokens(self) -> list[tuple[int, int] | None]:
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
- list[tuple[int, int] | None]
186
- A list of tuples, where each tuple contains the start and end token
187
- indices for a turn.
374
+ int
375
+ The token count for the input.
188
376
  """
189
- return [turn.tokens for turn in self._turns]
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,
@@ -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(
@@ -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,