chatlas 0.13.1__tar.gz → 0.13.2__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 (174) hide show
  1. {chatlas-0.13.1 → chatlas-0.13.2}/CHANGELOG.md +10 -0
  2. {chatlas-0.13.1 → chatlas-0.13.2}/PKG-INFO +4 -1
  3. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_content.py +32 -2
  4. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_databricks.py +7 -1
  5. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_openai.py +7 -1
  6. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_version.py +2 -2
  7. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/anthropic/_client.py +2 -2
  8. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/anthropic/_submit.py +10 -12
  9. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/openai/_client.py +1 -1
  10. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/openai/_submit.py +32 -32
  11. {chatlas-0.13.1 → chatlas-0.13.2}/pyproject.toml +4 -1
  12. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_content_tools.py +92 -0
  13. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_azure.py +17 -9
  14. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_databricks.py +10 -0
  15. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_tokens.py +1 -1
  16. {chatlas-0.13.1 → chatlas-0.13.2}/.github/workflows/check-update-types.yml +0 -0
  17. {chatlas-0.13.1 → chatlas-0.13.2}/.github/workflows/docs-publish.yml +0 -0
  18. {chatlas-0.13.1 → chatlas-0.13.2}/.github/workflows/release.yml +0 -0
  19. {chatlas-0.13.1 → chatlas-0.13.2}/.github/workflows/test.yml +0 -0
  20. {chatlas-0.13.1 → chatlas-0.13.2}/.github/workflows/update-pricing.yml +0 -0
  21. {chatlas-0.13.1 → chatlas-0.13.2}/.gitignore +0 -0
  22. {chatlas-0.13.1 → chatlas-0.13.2}/.vscode/extensions.json +0 -0
  23. {chatlas-0.13.1 → chatlas-0.13.2}/.vscode/settings.json +0 -0
  24. {chatlas-0.13.1 → chatlas-0.13.2}/CLAUDE.md +0 -0
  25. {chatlas-0.13.1 → chatlas-0.13.2}/LICENSE +0 -0
  26. {chatlas-0.13.1 → chatlas-0.13.2}/Makefile +0 -0
  27. {chatlas-0.13.1 → chatlas-0.13.2}/README.md +0 -0
  28. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/__init__.py +0 -0
  29. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_auto.py +0 -0
  30. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_batch_chat.py +0 -0
  31. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_batch_job.py +0 -0
  32. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_callbacks.py +0 -0
  33. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_chat.py +0 -0
  34. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_content_image.py +0 -0
  35. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_content_pdf.py +0 -0
  36. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_display.py +0 -0
  37. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_interpolate.py +0 -0
  38. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_live_render.py +0 -0
  39. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_logging.py +0 -0
  40. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_mcp_manager.py +0 -0
  41. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_merge.py +0 -0
  42. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider.py +0 -0
  43. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_anthropic.py +0 -0
  44. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_cloudflare.py +0 -0
  45. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_deepseek.py +0 -0
  46. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_github.py +0 -0
  47. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_google.py +0 -0
  48. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_groq.py +0 -0
  49. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_huggingface.py +0 -0
  50. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_mistral.py +0 -0
  51. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_ollama.py +0 -0
  52. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_openrouter.py +0 -0
  53. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_perplexity.py +0 -0
  54. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_portkey.py +0 -0
  55. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_provider_snowflake.py +0 -0
  56. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_tokens.py +0 -0
  57. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_tokens_old.py +0 -0
  58. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_tools.py +0 -0
  59. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_turn.py +0 -0
  60. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_typing_extensions.py +0 -0
  61. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/_utils.py +0 -0
  62. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/data/prices.json +0 -0
  63. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/py.typed +0 -0
  64. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/__init__.py +0 -0
  65. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/anthropic/__init__.py +0 -0
  66. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/anthropic/_client_bedrock.py +0 -0
  67. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/google/__init__.py +0 -0
  68. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/google/_client.py +0 -0
  69. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/google/_submit.py +0 -0
  70. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/openai/__init__.py +0 -0
  71. {chatlas-0.13.1 → chatlas-0.13.2}/chatlas/types/openai/_client_azure.py +0 -0
  72. {chatlas-0.13.1 → chatlas-0.13.2}/docs/.gitignore +0 -0
  73. {chatlas-0.13.1 → chatlas-0.13.2}/docs/_extensions/machow/interlinks/.gitignore +0 -0
  74. {chatlas-0.13.1 → chatlas-0.13.2}/docs/_extensions/machow/interlinks/_extension.yml +0 -0
  75. {chatlas-0.13.1 → chatlas-0.13.2}/docs/_extensions/machow/interlinks/interlinks.lua +0 -0
  76. {chatlas-0.13.1 → chatlas-0.13.2}/docs/_quarto.yml +0 -0
  77. {chatlas-0.13.1 → chatlas-0.13.2}/docs/_sidebar.yml +0 -0
  78. {chatlas-0.13.1 → chatlas-0.13.2}/docs/congressional-assets.png +0 -0
  79. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/async.qmd +0 -0
  80. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/chat.qmd +0 -0
  81. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/chatbots.qmd +0 -0
  82. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/debug.qmd +0 -0
  83. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/models.qmd +0 -0
  84. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/monitor.qmd +0 -0
  85. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/parameters.qmd +0 -0
  86. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/stream.qmd +0 -0
  87. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/structured-data.qmd +0 -0
  88. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/system-prompt.qmd +0 -0
  89. {chatlas-0.13.1 → chatlas-0.13.2}/docs/get-started/tools.qmd +0 -0
  90. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chat-app.png +0 -0
  91. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chat-console.mp4 +0 -0
  92. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chat-console.png +0 -0
  93. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chat-notebook.mp4 +0 -0
  94. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chat-parameters.png +0 -0
  95. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chatbot-gradio.png +0 -0
  96. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chatbot-shiny.png +0 -0
  97. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chatbot-streamlit.png +0 -0
  98. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chatbot-textual.png +0 -0
  99. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/chatlas-hello.png +0 -0
  100. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/client-parameters.png +0 -0
  101. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/congressional-assets.png +0 -0
  102. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/hello-chat-console.png +0 -0
  103. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/model-parameters.png +0 -0
  104. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/model-type-hints.png +0 -0
  105. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/posit-logo.png +0 -0
  106. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/shiny-mcp-run-python.png +0 -0
  107. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/shiny-tool-call-display.png +0 -0
  108. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/shiny-tool-call-map.png +0 -0
  109. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/tool-calling-right.svg +0 -0
  110. {chatlas-0.13.1 → chatlas-0.13.2}/docs/images/tool-calling-wrong.svg +0 -0
  111. {chatlas-0.13.1 → chatlas-0.13.2}/docs/index.qmd +0 -0
  112. {chatlas-0.13.1 → chatlas-0.13.2}/docs/logos/hero/hero-old.png +0 -0
  113. {chatlas-0.13.1 → chatlas-0.13.2}/docs/logos/hero/hero.png +0 -0
  114. {chatlas-0.13.1 → chatlas-0.13.2}/docs/logos/hex/logo.png +0 -0
  115. {chatlas-0.13.1 → chatlas-0.13.2}/docs/logos/small/logo.png +0 -0
  116. {chatlas-0.13.1 → chatlas-0.13.2}/docs/misc/RAG.qmd +0 -0
  117. {chatlas-0.13.1 → chatlas-0.13.2}/docs/misc/examples.qmd +0 -0
  118. {chatlas-0.13.1 → chatlas-0.13.2}/docs/misc/mcp-tools.qmd +0 -0
  119. {chatlas-0.13.1 → chatlas-0.13.2}/docs/misc/vocabulary.qmd +0 -0
  120. {chatlas-0.13.1 → chatlas-0.13.2}/docs/structured-data/article-summary.qmd +0 -0
  121. {chatlas-0.13.1 → chatlas-0.13.2}/docs/structured-data/classification.qmd +0 -0
  122. {chatlas-0.13.1 → chatlas-0.13.2}/docs/structured-data/entity-recognition.qmd +0 -0
  123. {chatlas-0.13.1 → chatlas-0.13.2}/docs/structured-data/multi-modal.qmd +0 -0
  124. {chatlas-0.13.1 → chatlas-0.13.2}/docs/structured-data/sentiment-analysis.qmd +0 -0
  125. {chatlas-0.13.1 → chatlas-0.13.2}/docs/styles.scss +0 -0
  126. {chatlas-0.13.1 → chatlas-0.13.2}/docs/tool-calling/approval.qmd +0 -0
  127. {chatlas-0.13.1 → chatlas-0.13.2}/docs/tool-calling/displays.qmd +0 -0
  128. {chatlas-0.13.1 → chatlas-0.13.2}/docs/tool-calling/how-it-works.qmd +0 -0
  129. {chatlas-0.13.1 → chatlas-0.13.2}/docs/why-chatlas.qmd +0 -0
  130. {chatlas-0.13.1 → chatlas-0.13.2}/pytest.ini +0 -0
  131. {chatlas-0.13.1 → chatlas-0.13.2}/scripts/_generate_anthropic_types.py +0 -0
  132. {chatlas-0.13.1 → chatlas-0.13.2}/scripts/_generate_google_types.py +0 -0
  133. {chatlas-0.13.1 → chatlas-0.13.2}/scripts/_generate_openai_types.py +0 -0
  134. {chatlas-0.13.1 → chatlas-0.13.2}/scripts/_utils.py +0 -0
  135. {chatlas-0.13.1 → chatlas-0.13.2}/scripts/main.py +0 -0
  136. {chatlas-0.13.1 → chatlas-0.13.2}/tests/__init__.py +0 -0
  137. {chatlas-0.13.1 → chatlas-0.13.2}/tests/__snapshots__/test_chat.ambr +0 -0
  138. {chatlas-0.13.1 → chatlas-0.13.2}/tests/apples.pdf +0 -0
  139. {chatlas-0.13.1 → chatlas-0.13.2}/tests/batch/country-capitals-structured.json +0 -0
  140. {chatlas-0.13.1 → chatlas-0.13.2}/tests/batch/country-capitals.json +0 -0
  141. {chatlas-0.13.1 → chatlas-0.13.2}/tests/conftest.py +0 -0
  142. {chatlas-0.13.1 → chatlas-0.13.2}/tests/images/dice.png +0 -0
  143. {chatlas-0.13.1 → chatlas-0.13.2}/tests/mcp_servers/http_add.py +0 -0
  144. {chatlas-0.13.1 → chatlas-0.13.2}/tests/mcp_servers/http_current_date.py +0 -0
  145. {chatlas-0.13.1 → chatlas-0.13.2}/tests/mcp_servers/stdio_current_date.py +0 -0
  146. {chatlas-0.13.1 → chatlas-0.13.2}/tests/mcp_servers/stdio_subtract_multiply.py +0 -0
  147. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_auto.py +0 -0
  148. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_batch_chat.py +0 -0
  149. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_callbacks.py +0 -0
  150. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_chat.py +0 -0
  151. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_content.py +0 -0
  152. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_content_html.py +0 -0
  153. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_content_image.py +0 -0
  154. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_content_pdf.py +0 -0
  155. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_interpolate.py +0 -0
  156. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_mcp_client.py +0 -0
  157. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_anthropic.py +0 -0
  158. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_bedrock.py +0 -0
  159. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_cloudflare.py +0 -0
  160. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_deepseek.py +0 -0
  161. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_github.py +0 -0
  162. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_google.py +0 -0
  163. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_huggingface.py +0 -0
  164. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_mistral.py +0 -0
  165. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_openai.py +0 -0
  166. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_openrouter.py +0 -0
  167. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_portkey.py +0 -0
  168. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_provider_snowflake.py +0 -0
  169. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_register_tool_models.py +0 -0
  170. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_set_model_params.py +0 -0
  171. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_tool_from_mcp.py +0 -0
  172. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_tools_enhanced.py +0 -0
  173. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_turns.py +0 -0
  174. {chatlas-0.13.1 → chatlas-0.13.2}/tests/test_utils_merge.py +0 -0
@@ -7,6 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
  -->
9
9
 
10
+ ## [0.13.2] - 2025-10-02
11
+
12
+ ### Improvements
13
+
14
+ * `ContentToolResult`'s `.get_model_value()` method now calls `.to_json(orient="record")` (instead of `.to_json()`) when relevant. As a result, if a tool call returns a Pandas `DataFrame` (or similar), the model now receives a less confusing (and smaller) JSON format. (#183)
15
+
16
+ ### Bug fixes
17
+
18
+ * `ChatAzureOpenAI()` and `ChatDatabricks()` now work as expected when a `OPENAI_API_KEY` environment variable isn't present. (#185)
19
+
10
20
  ## [0.13.1] - 2025-09-18
11
21
 
12
22
  ### Bug fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatlas
3
- Version: 0.13.1
3
+ Version: 0.13.2
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
@@ -38,9 +38,12 @@ Requires-Dist: databricks-sdk; extra == 'dev'
38
38
  Requires-Dist: google-genai>=1.14.0; extra == 'dev'
39
39
  Requires-Dist: htmltools; extra == 'dev'
40
40
  Requires-Dist: matplotlib; extra == 'dev'
41
+ Requires-Dist: narwhals; extra == 'dev'
41
42
  Requires-Dist: numpy>1.24.4; extra == 'dev'
42
43
  Requires-Dist: openai; extra == 'dev'
44
+ Requires-Dist: pandas; extra == 'dev'
43
45
  Requires-Dist: pillow; extra == 'dev'
46
+ Requires-Dist: polars; extra == 'dev'
44
47
  Requires-Dist: python-dotenv; extra == 'dev'
45
48
  Requires-Dist: ruff>=0.6.5; extra == 'dev'
46
49
  Requires-Dist: shiny; extra == 'dev'
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
4
+ import warnings
3
5
  from pprint import pformat
4
- from typing import TYPE_CHECKING, Any, Literal, Optional, Union
6
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
5
7
 
6
8
  import orjson
7
9
  from pydantic import BaseModel, ConfigDict
@@ -465,8 +467,36 @@ class ContentToolResult(Content):
465
467
 
466
468
  @staticmethod
467
469
  def _to_json(value: Any) -> object:
470
+ if hasattr(value, "to_pandas") and callable(value.to_pandas):
471
+ # Many (most?) df libs (polars, pyarrow, ...) have a .to_pandas()
472
+ # method, and pandas has a .to_json() method
473
+ value = value.to_pandas()
474
+
468
475
  if hasattr(value, "to_json") and callable(value.to_json):
469
- return value.to_json()
476
+ # pandas defaults to "columns", which is not ideal for LLMs
477
+ # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html
478
+ sig = inspect.signature(value.to_json)
479
+ if "orient" in list(sig.parameters.keys()):
480
+ return value.to_json(orient="records")
481
+ else:
482
+ return value.to_json()
483
+
484
+ # Support for df libs (beyond those with a .to_pandas() method)
485
+ if hasattr(value, "__narwhals_dataframe__"):
486
+ try:
487
+ import narwhals
488
+
489
+ val = cast(narwhals.DataFrame, narwhals.from_native(value))
490
+ return val.to_pandas().to_json(orient="records")
491
+ except ImportError:
492
+ warnings.warn(
493
+ f"Tool result object of type {type(value)} appears to be a "
494
+ "narwhals-compatible DataFrame. If you run into issues with "
495
+ "the LLM not understanding this value, try installing narwhals: "
496
+ "`pip install narwhals`.",
497
+ ImportWarning,
498
+ stacklevel=2,
499
+ )
470
500
 
471
501
  if hasattr(value, "to_dict") and callable(value.to_dict):
472
502
  value = value.to_dict()
@@ -106,7 +106,13 @@ class DatabricksProvider(OpenAIProvider):
106
106
  import httpx
107
107
  from openai import AsyncOpenAI
108
108
 
109
- super().__init__(name=name, model=model)
109
+ super().__init__(
110
+ name=name,
111
+ model=model,
112
+ # The OpenAI() constructor will fail if no API key is present.
113
+ # However, a dummy value is fine -- WorkspaceClient() handles the auth.
114
+ api_key="not-used",
115
+ )
110
116
 
111
117
  self._seed = None
112
118
 
@@ -884,7 +884,13 @@ class OpenAIAzureProvider(OpenAIProvider):
884
884
  model: Optional[str] = "UnusedValue",
885
885
  kwargs: Optional["ChatAzureClientArgs"] = None,
886
886
  ):
887
- super().__init__(name=name, model=deployment_id)
887
+ super().__init__(
888
+ name=name,
889
+ model=deployment_id,
890
+ # The OpenAI() constructor will fail if no API key is present.
891
+ # However, a dummy value is fine -- AzureOpenAI() handles the auth.
892
+ api_key=api_key or "not-used",
893
+ )
888
894
 
889
895
  self._seed = seed
890
896
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.13.1'
32
- __version_tuple__ = version_tuple = (0, 13, 1)
31
+ __version__ = version = '0.13.2'
32
+ __version_tuple__ = version_tuple = (0, 13, 2)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -3,7 +3,7 @@
3
3
  # ---------------------------------------------------------
4
4
 
5
5
 
6
- from typing import Mapping, Optional, TypedDict, Union
6
+ from typing import Mapping, Optional, TypedDict
7
7
 
8
8
  import anthropic
9
9
  import httpx
@@ -13,7 +13,7 @@ class ChatClientArgs(TypedDict, total=False):
13
13
  api_key: str | None
14
14
  auth_token: str | None
15
15
  base_url: str | httpx.URL | None
16
- timeout: Union[float, anthropic.Timeout, None, anthropic.NotGiven]
16
+ timeout: float | anthropic.Timeout | None | anthropic.NotGiven
17
17
  max_retries: int
18
18
  default_headers: Optional[Mapping[str, str]]
19
19
  default_query: Optional[Mapping[str, object]]
@@ -47,26 +47,24 @@ class SubmitInputArgs(TypedDict, total=False):
47
47
  ],
48
48
  str,
49
49
  ]
50
- service_tier: Union[Literal["auto", "standard_only"], anthropic.NotGiven]
51
- stop_sequences: Union[Sequence[str], anthropic.NotGiven]
52
- stream: Union[Literal[False], Literal[True], anthropic.NotGiven]
50
+ service_tier: Union[Literal["auto", "standard_only"], anthropic.Omit]
51
+ stop_sequences: Union[Sequence[str], anthropic.Omit]
52
+ stream: Union[Literal[False], Literal[True], anthropic.Omit]
53
53
  system: Union[
54
- str,
55
- Iterable[anthropic.types.text_block_param.TextBlockParam],
56
- anthropic.NotGiven,
54
+ str, Iterable[anthropic.types.text_block_param.TextBlockParam], anthropic.Omit
57
55
  ]
58
- temperature: float | anthropic.NotGiven
56
+ temperature: float | anthropic.Omit
59
57
  thinking: Union[
60
58
  anthropic.types.thinking_config_enabled_param.ThinkingConfigEnabledParam,
61
59
  anthropic.types.thinking_config_disabled_param.ThinkingConfigDisabledParam,
62
- anthropic.NotGiven,
60
+ anthropic.Omit,
63
61
  ]
64
62
  tool_choice: Union[
65
63
  anthropic.types.tool_choice_auto_param.ToolChoiceAutoParam,
66
64
  anthropic.types.tool_choice_any_param.ToolChoiceAnyParam,
67
65
  anthropic.types.tool_choice_tool_param.ToolChoiceToolParam,
68
66
  anthropic.types.tool_choice_none_param.ToolChoiceNoneParam,
69
- anthropic.NotGiven,
67
+ anthropic.Omit,
70
68
  ]
71
69
  tools: Union[
72
70
  Iterable[
@@ -79,10 +77,10 @@ class SubmitInputArgs(TypedDict, total=False):
79
77
  anthropic.types.web_search_tool_20250305_param.WebSearchTool20250305Param,
80
78
  ]
81
79
  ],
82
- anthropic.NotGiven,
80
+ anthropic.Omit,
83
81
  ]
84
- top_k: int | anthropic.NotGiven
85
- top_p: float | anthropic.NotGiven
82
+ top_k: int | anthropic.Omit
83
+ top_p: float | anthropic.Omit
86
84
  extra_headers: Optional[Mapping[str, Union[str, anthropic.Omit]]]
87
85
  extra_query: Optional[Mapping[str, object]]
88
86
  extra_body: object | None
@@ -16,7 +16,7 @@ class ChatClientArgs(TypedDict, total=False):
16
16
  webhook_secret: str | None
17
17
  base_url: str | httpx.URL | None
18
18
  websocket_base_url: str | httpx.URL | None
19
- timeout: Union[float, openai.Timeout, None, openai.NotGiven]
19
+ timeout: float | openai.Timeout | None | openai.NotGiven
20
20
  max_retries: int
21
21
  default_headers: Optional[Mapping[str, str]]
22
22
  default_query: Optional[Mapping[str, object]]
@@ -108,61 +108,61 @@ class SubmitInputArgs(TypedDict, total=False):
108
108
  audio: Union[
109
109
  openai.types.chat.chat_completion_audio_param.ChatCompletionAudioParam,
110
110
  None,
111
- openai.NotGiven,
111
+ openai.Omit,
112
112
  ]
113
- frequency_penalty: Union[float, None, openai.NotGiven]
113
+ frequency_penalty: Union[float, None, openai.Omit]
114
114
  function_call: Union[
115
115
  Literal["none", "auto"],
116
116
  openai.types.chat.chat_completion_function_call_option_param.ChatCompletionFunctionCallOptionParam,
117
- openai.NotGiven,
117
+ openai.Omit,
118
118
  ]
119
119
  functions: Union[
120
- Iterable[openai.types.chat.completion_create_params.Function], openai.NotGiven
120
+ Iterable[openai.types.chat.completion_create_params.Function], openai.Omit
121
121
  ]
122
- logit_bias: Union[dict[str, int], None, openai.NotGiven]
123
- logprobs: Union[bool, None, openai.NotGiven]
124
- max_completion_tokens: Union[int, None, openai.NotGiven]
125
- max_tokens: Union[int, None, openai.NotGiven]
126
- metadata: Union[dict[str, str], None, openai.NotGiven]
127
- modalities: Union[list[Literal["text", "audio"]], None, openai.NotGiven]
128
- n: Union[int, None, openai.NotGiven]
129
- parallel_tool_calls: bool | openai.NotGiven
122
+ logit_bias: Union[dict[str, int], None, openai.Omit]
123
+ logprobs: Union[bool, None, openai.Omit]
124
+ max_completion_tokens: Union[int, None, openai.Omit]
125
+ max_tokens: Union[int, None, openai.Omit]
126
+ metadata: Union[dict[str, str], None, openai.Omit]
127
+ modalities: Union[list[Literal["text", "audio"]], None, openai.Omit]
128
+ n: Union[int, None, openai.Omit]
129
+ parallel_tool_calls: bool | openai.Omit
130
130
  prediction: Union[
131
131
  openai.types.chat.chat_completion_prediction_content_param.ChatCompletionPredictionContentParam,
132
132
  None,
133
- openai.NotGiven,
133
+ openai.Omit,
134
134
  ]
135
- presence_penalty: Union[float, None, openai.NotGiven]
136
- prompt_cache_key: str | openai.NotGiven
135
+ presence_penalty: Union[float, None, openai.Omit]
136
+ prompt_cache_key: str | openai.Omit
137
137
  reasoning_effort: Union[
138
- Literal["minimal", "low", "medium", "high"], None, openai.NotGiven
138
+ Literal["minimal", "low", "medium", "high"], None, openai.Omit
139
139
  ]
140
140
  response_format: Union[
141
141
  openai.types.shared_params.response_format_text.ResponseFormatText,
142
142
  openai.types.shared_params.response_format_json_schema.ResponseFormatJSONSchema,
143
143
  openai.types.shared_params.response_format_json_object.ResponseFormatJSONObject,
144
- openai.NotGiven,
144
+ openai.Omit,
145
145
  ]
146
- safety_identifier: str | openai.NotGiven
147
- seed: Union[int, None, openai.NotGiven]
146
+ safety_identifier: str | openai.Omit
147
+ seed: Union[int, None, openai.Omit]
148
148
  service_tier: Union[
149
- Literal["auto", "default", "flex", "scale", "priority"], None, openai.NotGiven
149
+ Literal["auto", "default", "flex", "scale", "priority"], None, openai.Omit
150
150
  ]
151
- stop: Union[str, None, Sequence[str], openai.NotGiven]
152
- store: Union[bool, None, openai.NotGiven]
153
- stream: Union[Literal[False], None, Literal[True], openai.NotGiven]
151
+ stop: Union[str, None, Sequence[str], openai.Omit]
152
+ store: Union[bool, None, openai.Omit]
153
+ stream: Union[Literal[False], None, Literal[True], openai.Omit]
154
154
  stream_options: Union[
155
155
  openai.types.chat.chat_completion_stream_options_param.ChatCompletionStreamOptionsParam,
156
156
  None,
157
- openai.NotGiven,
157
+ openai.Omit,
158
158
  ]
159
- temperature: Union[float, None, openai.NotGiven]
159
+ temperature: Union[float, None, openai.Omit]
160
160
  tool_choice: Union[
161
161
  Literal["none", "auto", "required"],
162
162
  openai.types.chat.chat_completion_allowed_tool_choice_param.ChatCompletionAllowedToolChoiceParam,
163
163
  openai.types.chat.chat_completion_named_tool_choice_param.ChatCompletionNamedToolChoiceParam,
164
164
  openai.types.chat.chat_completion_named_tool_choice_custom_param.ChatCompletionNamedToolChoiceCustomParam,
165
- openai.NotGiven,
165
+ openai.Omit,
166
166
  ]
167
167
  tools: Union[
168
168
  Iterable[
@@ -171,14 +171,14 @@ class SubmitInputArgs(TypedDict, total=False):
171
171
  openai.types.chat.chat_completion_custom_tool_param.ChatCompletionCustomToolParam,
172
172
  ]
173
173
  ],
174
- openai.NotGiven,
174
+ openai.Omit,
175
175
  ]
176
- top_logprobs: Union[int, None, openai.NotGiven]
177
- top_p: Union[float, None, openai.NotGiven]
178
- user: str | openai.NotGiven
179
- verbosity: Union[Literal["low", "medium", "high"], None, openai.NotGiven]
176
+ top_logprobs: Union[int, None, openai.Omit]
177
+ top_p: Union[float, None, openai.Omit]
178
+ user: str | openai.Omit
179
+ verbosity: Union[Literal["low", "medium", "high"], None, openai.Omit]
180
180
  web_search_options: (
181
- openai.types.chat.completion_create_params.WebSearchOptions | openai.NotGiven
181
+ openai.types.chat.completion_create_params.WebSearchOptions | openai.Omit
182
182
  )
183
183
  extra_headers: Optional[Mapping[str, Union[str, openai.Omit]]]
184
184
  extra_query: Optional[Mapping[str, object]]
@@ -48,7 +48,11 @@ dev = [
48
48
  "matplotlib",
49
49
  "Pillow",
50
50
  "shiny",
51
+ "htmltools",
51
52
  "shinychat",
53
+ "narwhals",
54
+ "pandas",
55
+ "polars",
52
56
  "openai",
53
57
  "anthropic[bedrock]",
54
58
  "google-genai>=1.14.0",
@@ -58,7 +62,6 @@ dev = [
58
62
  "snowflake-ml-python>=1.8.4",
59
63
  # torch (a dependency of snowflake-ml-python) is not yet compatible with Python >3.11
60
64
  "torch;python_version<='3.11'",
61
- "htmltools",
62
65
  "tenacity"
63
66
  ]
64
67
  docs = [
@@ -1,5 +1,7 @@
1
1
  from typing import Any, Optional, Union
2
+ from unittest.mock import Mock
2
3
 
4
+ import orjson
3
5
  import pytest
4
6
 
5
7
  from chatlas import ChatOpenAI
@@ -403,3 +405,93 @@ def test_content_tool_request_serializable():
403
405
  assert parsed.tool is not None
404
406
  assert parsed.tool.name == "add"
405
407
  assert parsed.tool.description == "Add two numbers"
408
+
409
+
410
+ def test_content_tool_result_pandas_dataframe():
411
+ """Test ContentToolResult with pandas DataFrame using orient='records'"""
412
+ pandas = pytest.importorskip("pandas")
413
+
414
+ # Create a simple pandas DataFrame
415
+ df = pandas.DataFrame(
416
+ {"name": ["Alice", "Bob"], "age": [25, 30], "city": ["New York", "London"]}
417
+ )
418
+
419
+ # Create ContentToolResult with DataFrame value
420
+ result = ContentToolResult(value=df).get_model_value()
421
+ expected = df.to_json(orient="records")
422
+ assert result == expected
423
+
424
+ parsed = orjson.loads(str(result))
425
+ assert isinstance(parsed, list)
426
+ assert len(parsed) == 2
427
+ assert parsed[0] == {"name": "Alice", "age": 25, "city": "New York"}
428
+ assert parsed[1] == {"name": "Bob", "age": 30, "city": "London"}
429
+
430
+
431
+ def test_content_tool_result_object_with_to_pandas():
432
+ """Test ContentToolResult with objects that have .to_pandas() method"""
433
+ pandas = pytest.importorskip("pandas")
434
+
435
+ # Create mock object with to_pandas method (like Polars, PyArrow)
436
+ mock_df_lib = Mock()
437
+ pandas_df = pandas.DataFrame({"x": [1, 2, 3], "y": ["a", "b", "c"]})
438
+ mock_df_lib.to_pandas.return_value = pandas_df
439
+
440
+ result = ContentToolResult(value=mock_df_lib).get_model_value()
441
+ mock_df_lib.to_pandas.assert_called_once()
442
+ expected = pandas_df.to_json(orient="records")
443
+ assert result == expected
444
+
445
+
446
+ def test_content_tool_result_narwhals_dataframe():
447
+ """Test ContentToolResult with narwhals DataFrame"""
448
+ narwhals = pytest.importorskip("narwhals")
449
+ pandas = pytest.importorskip("pandas")
450
+
451
+ pandas_df = pandas.DataFrame({"a": [1, 2], "b": ["x", "y"]})
452
+ nw_df = narwhals.from_native(pandas_df)
453
+ result = ContentToolResult(value=nw_df).get_model_value()
454
+ expected = pandas_df.to_json(orient="records")
455
+ assert result == expected
456
+
457
+
458
+ def test_content_tool_result_object_with_to_dict():
459
+ """Test ContentToolResult with objects that have to_dict method"""
460
+ # Mock object with to_dict method but no to_pandas or to_json
461
+ mock_obj = Mock(spec=["to_dict"])
462
+ mock_obj.to_dict.return_value = {"key": "value"}
463
+ result = ContentToolResult(value=mock_obj).get_model_value()
464
+ mock_obj.to_dict.assert_called_once()
465
+ # Result should be JSON string representation (orjson format)
466
+ assert result == '{"key":"value"}'
467
+
468
+
469
+ def test_content_tool_result_string_passthrough():
470
+ """Test ContentToolResult with string values (special case - passed through as-is)"""
471
+ result = ContentToolResult(value="plain string").get_model_value()
472
+ assert result == "plain string"
473
+
474
+
475
+ def test_content_tool_result_fallback_serialization():
476
+ """Test ContentToolResult fallback for objects without special methods"""
477
+ # Regular object without to_json, to_pandas, or to_dict (non-string to avoid the string special case)
478
+ result = ContentToolResult(value={"key": "value"}).get_model_value()
479
+ assert result == '{"key":"value"}'
480
+
481
+
482
+ def test_content_tool_result_explicit_json_mode():
483
+ """Test ContentToolResult with explicit JSON mode forces _to_json for non-strings"""
484
+ # Test with non-string object and explicit JSON mode
485
+ result = ContentToolResult(
486
+ value={"key": "value"},
487
+ model_format="json",
488
+ ).get_model_value()
489
+ # With explicit JSON mode, objects get JSON-encoded
490
+ assert result == '{"key":"value"}'
491
+ # Test that strings still get special treatment even in JSON mode
492
+ string_result = ContentToolResult(
493
+ value="plain string",
494
+ model_format="json",
495
+ ).get_model_value()
496
+ # Strings are still returned as-is even in JSON mode (current behavior)
497
+ assert string_result == "plain string"
@@ -9,14 +9,17 @@ if do_test.lower() == "false":
9
9
  pytest.skip("Skipping Azure tests", allow_module_level=True)
10
10
 
11
11
 
12
- def test_azure_simple_request():
13
- chat = ChatAzureOpenAI(
14
- system_prompt="Be as terse as possible; no punctuation",
12
+ def chat_func(system_prompt: str = "Be as terse as possible; no punctuation"):
13
+ return ChatAzureOpenAI(
14
+ system_prompt=system_prompt,
15
15
  endpoint="https://chatlas-testing.openai.azure.com",
16
16
  deployment_id="gpt-4o-mini",
17
17
  api_version="2024-08-01-preview",
18
18
  )
19
19
 
20
+
21
+ def test_azure_simple_request():
22
+ chat = chat_func()
20
23
  response = chat.chat("What is 1 + 1?")
21
24
  assert "2" == response.get_content()
22
25
  turn = chat.get_last_turn()
@@ -27,15 +30,20 @@ def test_azure_simple_request():
27
30
 
28
31
  @pytest.mark.asyncio
29
32
  async def test_azure_simple_request_async():
30
- chat = ChatAzureOpenAI(
31
- system_prompt="Be as terse as possible; no punctuation",
32
- endpoint="https://chatlas-testing.openai.azure.com",
33
- deployment_id="gpt-4o-mini",
34
- api_version="2024-08-01-preview",
35
- )
33
+ chat = chat_func()
36
34
 
37
35
  response = await chat.chat_async("What is 1 + 1?")
38
36
  assert "2" == await response.get_content()
39
37
  turn = chat.get_last_turn()
40
38
  assert turn is not None
41
39
  assert turn.tokens == (27, 2, 0)
40
+
41
+
42
+ def test_connect_without_openai_key(monkeypatch):
43
+ # Ensure OPENAI_API_KEY is not set
44
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
45
+
46
+ # This should not raise an error
47
+ chat = chat_func()
48
+ assert chat is not None
49
+
@@ -1,4 +1,5 @@
1
1
  import pytest
2
+
2
3
  from chatlas import ChatDatabricks
3
4
 
4
5
  from .conftest import assert_turns_existing, assert_turns_system
@@ -82,3 +83,12 @@ def test_anthropic_empty_response():
82
83
  # def test_openai_pdf():
83
84
  # chat_fun = ChatDatabricks
84
85
  # assert_pdf_local(chat_fun)
86
+
87
+
88
+ def test_connect_without_openai_key(monkeypatch):
89
+ # Ensure OPENAI_API_KEY is not set
90
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
91
+
92
+ # This should not raise an error
93
+ chat = ChatDatabricks()
94
+ assert chat is not None
@@ -70,7 +70,7 @@ def test_token_count_method():
70
70
  chat = ChatAnthropic(model="claude-3-5-sonnet-20241022")
71
71
  assert chat.token_count("What is 1 + 1?") == 16
72
72
 
73
- chat = ChatGoogle(model="gemini-1.5-flash")
73
+ chat = ChatGoogle(model="gemini-2.5-flash")
74
74
  assert chat.token_count("What is 1 + 1?") == 9
75
75
 
76
76
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes