chatlas 0.11.0__tar.gz → 0.11.1__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.
Files changed (169) hide show
  1. {chatlas-0.11.0 → chatlas-0.11.1}/CHANGELOG.md +16 -0
  2. {chatlas-0.11.0 → chatlas-0.11.1}/PKG-INFO +1 -1
  3. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_chat.py +15 -7
  4. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_content.py +93 -3
  5. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_tools.py +25 -9
  6. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_typing_extensions.py +1 -1
  7. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_version.py +2 -2
  8. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/__init__.py +4 -0
  9. {chatlas-0.11.0 → chatlas-0.11.1}/docs/_quarto.yml +2 -0
  10. {chatlas-0.11.0 → chatlas-0.11.1}/tests/conftest.py +2 -6
  11. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_content_tools.py +48 -6
  12. chatlas-0.11.1/tests/test_register_tool_models.py +339 -0
  13. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_tool_from_mcp.py +4 -4
  14. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_tools_enhanced.py +40 -8
  15. {chatlas-0.11.0 → chatlas-0.11.1}/.github/workflows/check-update-types.yml +0 -0
  16. {chatlas-0.11.0 → chatlas-0.11.1}/.github/workflows/docs-publish.yml +0 -0
  17. {chatlas-0.11.0 → chatlas-0.11.1}/.github/workflows/release.yml +0 -0
  18. {chatlas-0.11.0 → chatlas-0.11.1}/.github/workflows/test.yml +0 -0
  19. {chatlas-0.11.0 → chatlas-0.11.1}/.github/workflows/update-pricing.yml +0 -0
  20. {chatlas-0.11.0 → chatlas-0.11.1}/.gitignore +0 -0
  21. {chatlas-0.11.0 → chatlas-0.11.1}/.vscode/extensions.json +0 -0
  22. {chatlas-0.11.0 → chatlas-0.11.1}/.vscode/settings.json +0 -0
  23. {chatlas-0.11.0 → chatlas-0.11.1}/CLAUDE.md +0 -0
  24. {chatlas-0.11.0 → chatlas-0.11.1}/LICENSE +0 -0
  25. {chatlas-0.11.0 → chatlas-0.11.1}/Makefile +0 -0
  26. {chatlas-0.11.0 → chatlas-0.11.1}/README.md +0 -0
  27. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/__init__.py +0 -0
  28. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_auto.py +0 -0
  29. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_callbacks.py +0 -0
  30. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_content_image.py +0 -0
  31. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_content_pdf.py +0 -0
  32. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_display.py +0 -0
  33. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_interpolate.py +0 -0
  34. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_live_render.py +0 -0
  35. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_logging.py +0 -0
  36. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_mcp_manager.py +0 -0
  37. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_merge.py +0 -0
  38. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider.py +0 -0
  39. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_anthropic.py +0 -0
  40. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_cloudflare.py +0 -0
  41. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_databricks.py +0 -0
  42. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_deepseek.py +0 -0
  43. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_github.py +0 -0
  44. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_google.py +0 -0
  45. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_groq.py +0 -0
  46. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_huggingface.py +0 -0
  47. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_mistral.py +0 -0
  48. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_ollama.py +0 -0
  49. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_openai.py +0 -0
  50. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_openrouter.py +0 -0
  51. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_perplexity.py +0 -0
  52. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_portkey.py +0 -0
  53. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_provider_snowflake.py +0 -0
  54. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_tokens.py +0 -0
  55. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_tokens_old.py +0 -0
  56. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_turn.py +0 -0
  57. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/_utils.py +0 -0
  58. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/data/prices.json +0 -0
  59. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/py.typed +0 -0
  60. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/anthropic/__init__.py +0 -0
  61. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/anthropic/_client.py +0 -0
  62. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/anthropic/_client_bedrock.py +0 -0
  63. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/anthropic/_submit.py +0 -0
  64. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/google/__init__.py +0 -0
  65. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/google/_client.py +0 -0
  66. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/google/_submit.py +0 -0
  67. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/openai/__init__.py +0 -0
  68. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/openai/_client.py +0 -0
  69. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/openai/_client_azure.py +0 -0
  70. {chatlas-0.11.0 → chatlas-0.11.1}/chatlas/types/openai/_submit.py +0 -0
  71. {chatlas-0.11.0 → chatlas-0.11.1}/docs/.gitignore +0 -0
  72. {chatlas-0.11.0 → chatlas-0.11.1}/docs/_extensions/machow/interlinks/.gitignore +0 -0
  73. {chatlas-0.11.0 → chatlas-0.11.1}/docs/_extensions/machow/interlinks/_extension.yml +0 -0
  74. {chatlas-0.11.0 → chatlas-0.11.1}/docs/_extensions/machow/interlinks/interlinks.lua +0 -0
  75. {chatlas-0.11.0 → chatlas-0.11.1}/docs/_sidebar.yml +0 -0
  76. {chatlas-0.11.0 → chatlas-0.11.1}/docs/congressional-assets.png +0 -0
  77. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/async.qmd +0 -0
  78. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/chat.qmd +0 -0
  79. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/chatbots.qmd +0 -0
  80. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/debug.qmd +0 -0
  81. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/models.qmd +0 -0
  82. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/monitor.qmd +0 -0
  83. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/parameters.qmd +0 -0
  84. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/stream.qmd +0 -0
  85. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/structured-data.qmd +0 -0
  86. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/system-prompt.qmd +0 -0
  87. {chatlas-0.11.0 → chatlas-0.11.1}/docs/get-started/tools.qmd +0 -0
  88. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chat-app.png +0 -0
  89. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chat-console.mp4 +0 -0
  90. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chat-console.png +0 -0
  91. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chat-notebook.mp4 +0 -0
  92. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chat-parameters.png +0 -0
  93. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chatbot-gradio.png +0 -0
  94. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chatbot-shiny.png +0 -0
  95. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chatbot-streamlit.png +0 -0
  96. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chatbot-textual.png +0 -0
  97. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/chatlas-hello.png +0 -0
  98. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/client-parameters.png +0 -0
  99. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/congressional-assets.png +0 -0
  100. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/hello-chat-console.png +0 -0
  101. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/model-parameters.png +0 -0
  102. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/model-type-hints.png +0 -0
  103. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/posit-logo.png +0 -0
  104. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/shiny-mcp-run-python.png +0 -0
  105. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/shiny-tool-call-display.png +0 -0
  106. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/shiny-tool-call-map.png +0 -0
  107. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/tool-calling-right.svg +0 -0
  108. {chatlas-0.11.0 → chatlas-0.11.1}/docs/images/tool-calling-wrong.svg +0 -0
  109. {chatlas-0.11.0 → chatlas-0.11.1}/docs/index.qmd +0 -0
  110. {chatlas-0.11.0 → chatlas-0.11.1}/docs/logos/hero/hero-old.png +0 -0
  111. {chatlas-0.11.0 → chatlas-0.11.1}/docs/logos/hero/hero.png +0 -0
  112. {chatlas-0.11.0 → chatlas-0.11.1}/docs/logos/hex/logo.png +0 -0
  113. {chatlas-0.11.0 → chatlas-0.11.1}/docs/logos/small/logo.png +0 -0
  114. {chatlas-0.11.0 → chatlas-0.11.1}/docs/misc/RAG.qmd +0 -0
  115. {chatlas-0.11.0 → chatlas-0.11.1}/docs/misc/examples.qmd +0 -0
  116. {chatlas-0.11.0 → chatlas-0.11.1}/docs/misc/mcp-tools.qmd +0 -0
  117. {chatlas-0.11.0 → chatlas-0.11.1}/docs/misc/vocabulary.qmd +0 -0
  118. {chatlas-0.11.0 → chatlas-0.11.1}/docs/structured-data/article-summary.qmd +0 -0
  119. {chatlas-0.11.0 → chatlas-0.11.1}/docs/structured-data/classification.qmd +0 -0
  120. {chatlas-0.11.0 → chatlas-0.11.1}/docs/structured-data/entity-recognition.qmd +0 -0
  121. {chatlas-0.11.0 → chatlas-0.11.1}/docs/structured-data/multi-modal.qmd +0 -0
  122. {chatlas-0.11.0 → chatlas-0.11.1}/docs/structured-data/sentiment-analysis.qmd +0 -0
  123. {chatlas-0.11.0 → chatlas-0.11.1}/docs/styles.scss +0 -0
  124. {chatlas-0.11.0 → chatlas-0.11.1}/docs/tool-calling/approval.qmd +0 -0
  125. {chatlas-0.11.0 → chatlas-0.11.1}/docs/tool-calling/displays.qmd +0 -0
  126. {chatlas-0.11.0 → chatlas-0.11.1}/docs/tool-calling/how-it-works.qmd +0 -0
  127. {chatlas-0.11.0 → chatlas-0.11.1}/docs/why-chatlas.qmd +0 -0
  128. {chatlas-0.11.0 → chatlas-0.11.1}/pyproject.toml +0 -0
  129. {chatlas-0.11.0 → chatlas-0.11.1}/pytest.ini +0 -0
  130. {chatlas-0.11.0 → chatlas-0.11.1}/scripts/_generate_anthropic_types.py +0 -0
  131. {chatlas-0.11.0 → chatlas-0.11.1}/scripts/_generate_google_types.py +0 -0
  132. {chatlas-0.11.0 → chatlas-0.11.1}/scripts/_generate_openai_types.py +0 -0
  133. {chatlas-0.11.0 → chatlas-0.11.1}/scripts/_utils.py +0 -0
  134. {chatlas-0.11.0 → chatlas-0.11.1}/scripts/main.py +0 -0
  135. {chatlas-0.11.0 → chatlas-0.11.1}/tests/__init__.py +0 -0
  136. {chatlas-0.11.0 → chatlas-0.11.1}/tests/__snapshots__/test_chat.ambr +0 -0
  137. {chatlas-0.11.0 → chatlas-0.11.1}/tests/apples.pdf +0 -0
  138. {chatlas-0.11.0 → chatlas-0.11.1}/tests/images/dice.png +0 -0
  139. {chatlas-0.11.0 → chatlas-0.11.1}/tests/mcp_servers/http_add.py +0 -0
  140. {chatlas-0.11.0 → chatlas-0.11.1}/tests/mcp_servers/http_current_date.py +0 -0
  141. {chatlas-0.11.0 → chatlas-0.11.1}/tests/mcp_servers/stdio_current_date.py +0 -0
  142. {chatlas-0.11.0 → chatlas-0.11.1}/tests/mcp_servers/stdio_subtract_multiply.py +0 -0
  143. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_auto.py +0 -0
  144. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_callbacks.py +0 -0
  145. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_chat.py +0 -0
  146. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_content.py +0 -0
  147. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_content_html.py +0 -0
  148. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_content_image.py +0 -0
  149. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_content_pdf.py +0 -0
  150. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_interpolate.py +0 -0
  151. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_mcp_client.py +0 -0
  152. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_anthropic.py +0 -0
  153. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_azure.py +0 -0
  154. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_bedrock.py +0 -0
  155. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_cloudflare.py +0 -0
  156. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_databricks.py +0 -0
  157. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_deepseek.py +0 -0
  158. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_github.py +0 -0
  159. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_google.py +0 -0
  160. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_huggingface.py +0 -0
  161. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_mistral.py +0 -0
  162. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_openai.py +0 -0
  163. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_openrouter.py +0 -0
  164. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_portkey.py +0 -0
  165. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_provider_snowflake.py +0 -0
  166. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_set_model_params.py +0 -0
  167. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_tokens.py +0 -0
  168. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_turns.py +0 -0
  169. {chatlas-0.11.0 → chatlas-0.11.1}/tests/test_utils_merge.py +0 -0
@@ -7,6 +7,22 @@ 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.11.1] - 2025-08-29
11
+
12
+ ### New features
13
+
14
+ * `.register_tool()` gains a `name` parameter (useful for overriding the name of the function). (#162)
15
+
16
+ ### Bug fixes
17
+
18
+ * `ContentToolRequest` is (once again) serializable to/from JSON via Pydantic. (#164)
19
+ * `.register_tool(model=model)` no longer unexpectedly errors when `model` contains `pydantic.Field(alias='_my_alias')`. (#161)
20
+
21
+ ### Changes
22
+
23
+ * `.register_tool(annotations=annotations)` drops support for `mcp.types.ToolAnnotations()` and instead expects a dictionary of the same info. (#164)
24
+
25
+
10
26
  ## [0.11.0] - 2025-08-26
11
27
 
12
28
  ### New features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatlas
3
- Version: 0.11.0
3
+ Version: 0.11.1
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
@@ -34,6 +34,7 @@ from ._content import (
34
34
  ContentText,
35
35
  ContentToolRequest,
36
36
  ContentToolResult,
37
+ ToolInfo,
37
38
  )
38
39
  from ._display import (
39
40
  EchoDisplayOptions,
@@ -52,7 +53,7 @@ from ._typing_extensions import TypedDict, TypeGuard
52
53
  from ._utils import MISSING, MISSING_TYPE, html_escape, wrap_async
53
54
 
54
55
  if TYPE_CHECKING:
55
- from mcp.types import ToolAnnotations
56
+ from ._content import ToolAnnotations
56
57
 
57
58
 
58
59
  class TokensDict(TypedDict):
@@ -1537,6 +1538,7 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
1537
1538
  func: Callable[..., Any] | Callable[..., Awaitable[Any]],
1538
1539
  *,
1539
1540
  force: bool = False,
1541
+ name: Optional[str] = None,
1540
1542
  model: Optional[type[BaseModel]] = None,
1541
1543
  annotations: "Optional[ToolAnnotations]" = None,
1542
1544
  ):
@@ -1610,6 +1612,9 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
1610
1612
  force
1611
1613
  If `True`, overwrite any existing tool with the same name. If `False`
1612
1614
  (the default), raise an error if a tool with the same name already exists.
1615
+ name
1616
+ The name of the tool. If not provided, the name will be inferred from the
1617
+ `func`'s name (or the `model`'s name, if provided).
1613
1618
  model
1614
1619
  A Pydantic model that describes the input parameters for the function.
1615
1620
  If not provided, the model will be inferred from the function's type hints.
@@ -1618,14 +1623,13 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
1618
1623
  name and docstring of the function.
1619
1624
  annotations
1620
1625
  Additional properties that describe the tool and its behavior.
1621
- Should be a `from mcp.types import ToolAnnotations` instance.
1622
1626
 
1623
1627
  Raises
1624
1628
  ------
1625
1629
  ValueError
1626
1630
  If a tool with the same name already exists and `force` is `False`.
1627
1631
  """
1628
- tool = Tool.from_func(func, model=model, annotations=annotations)
1632
+ tool = Tool.from_func(func, name=name, model=model, annotations=annotations)
1629
1633
  if tool.name in self._tools and not force:
1630
1634
  raise ValueError(
1631
1635
  f"Tool with name '{tool.name}' is already registered. "
@@ -1933,7 +1937,9 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
1933
1937
  all_results: list[ContentToolResult] = []
1934
1938
  for x in turn.contents:
1935
1939
  if isinstance(x, ContentToolRequest):
1936
- x.tool = self._tools.get(x.name)
1940
+ tool = self._tools.get(x.name)
1941
+ if tool is not None:
1942
+ x.tool = ToolInfo.from_tool(tool)
1937
1943
  if echo == "output":
1938
1944
  self._echo_content(f"\n\n{x}\n\n")
1939
1945
  if content == "all":
@@ -1994,7 +2000,9 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
1994
2000
  all_results: list[ContentToolResult] = []
1995
2001
  for x in turn.contents:
1996
2002
  if isinstance(x, ContentToolRequest):
1997
- x.tool = self._tools.get(x.name)
2003
+ tool = self._tools.get(x.name)
2004
+ if tool is not None:
2005
+ x.tool = ToolInfo.from_tool(tool)
1998
2006
  if echo == "output":
1999
2007
  self._echo_content(f"\n\n{x}\n\n")
2000
2008
  if content == "all":
@@ -2152,7 +2160,7 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
2152
2160
  self._turns.extend([user_turn, turn])
2153
2161
 
2154
2162
  def _invoke_tool(self, request: ContentToolRequest):
2155
- tool = request.tool
2163
+ tool = self._tools.get(request.name)
2156
2164
  func = tool.func if tool is not None else None
2157
2165
 
2158
2166
  if func is None:
@@ -2200,7 +2208,7 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
2200
2208
  yield self._handle_tool_error_result(request, e)
2201
2209
 
2202
2210
  async def _invoke_tool_async(self, request: ContentToolRequest):
2203
- tool = request.tool
2211
+ tool = self._tools.get(request.name)
2204
2212
 
2205
2213
  if tool is None:
2206
2214
  yield self._handle_tool_error_result(
@@ -6,9 +6,59 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
6
6
  import orjson
7
7
  from pydantic import BaseModel, ConfigDict
8
8
 
9
+ from ._typing_extensions import NotRequired, TypedDict
10
+
9
11
  if TYPE_CHECKING:
10
12
  from ._tools import Tool
11
13
 
14
+
15
+ class ToolAnnotations(TypedDict, total=False):
16
+ """
17
+ Additional properties describing a Tool to clients.
18
+
19
+ NOTE: all properties in ToolAnnotations are **hints**.
20
+ They are not guaranteed to provide a faithful description of
21
+ tool behavior (including descriptive properties like `title`).
22
+
23
+ Clients should never make tool use decisions based on ToolAnnotations
24
+ received from untrusted servers.
25
+ """
26
+
27
+ title: NotRequired[str]
28
+ """A human-readable title for the tool."""
29
+
30
+ readOnlyHint: NotRequired[bool]
31
+ """
32
+ If true, the tool does not modify its environment.
33
+ Default: false
34
+ """
35
+
36
+ destructiveHint: NotRequired[bool]
37
+ """
38
+ If true, the tool may perform destructive updates to its environment.
39
+ If false, the tool performs only additive updates.
40
+ (This property is meaningful only when `readOnlyHint == false`)
41
+ Default: true
42
+ """
43
+
44
+ idempotentHint: NotRequired[bool]
45
+ """
46
+ If true, calling the tool repeatedly with the same arguments
47
+ will have no additional effect on the its environment.
48
+ (This property is meaningful only when `readOnlyHint == false`)
49
+ Default: false
50
+ """
51
+
52
+ openWorldHint: NotRequired[bool]
53
+ """
54
+ If true, this tool may interact with an "open world" of external
55
+ entities. If false, the tool's domain of interaction is closed.
56
+ For example, the world of a web search tool is open, whereas that
57
+ of a memory tool is not.
58
+ Default: true
59
+ """
60
+
61
+
12
62
  ImageContentTypes = Literal[
13
63
  "image/png",
14
64
  "image/jpeg",
@@ -19,6 +69,45 @@ ImageContentTypes = Literal[
19
69
  Allowable content types for images.
20
70
  """
21
71
 
72
+
73
+ class ToolInfo(BaseModel):
74
+ """
75
+ Serializable tool information
76
+
77
+ This contains only the serializable parts of a Tool that are needed
78
+ for ContentToolRequest to be JSON-serializable. This allows tool
79
+ metadata to be preserved without including the non-serializable
80
+ function reference.
81
+
82
+ Parameters
83
+ ----------
84
+ name
85
+ The name of the tool.
86
+ description
87
+ A description of what the tool does.
88
+ parameters
89
+ A dictionary describing the input parameters and their types.
90
+ annotations
91
+ Additional properties that describe the tool and its behavior.
92
+ """
93
+
94
+ name: str
95
+ description: str
96
+ parameters: dict[str, Any]
97
+ annotations: Optional[ToolAnnotations] = None
98
+
99
+ @classmethod
100
+ def from_tool(cls, tool: "Tool") -> "ToolInfo":
101
+ """Create a ToolInfo from a Tool instance."""
102
+ func_schema = tool.schema["function"]
103
+ return cls(
104
+ name=tool.name,
105
+ description=func_schema.get("description", ""),
106
+ parameters=func_schema.get("parameters", {}),
107
+ annotations=tool.annotations,
108
+ )
109
+
110
+
22
111
  ContentTypeEnum = Literal[
23
112
  "text",
24
113
  "image_remote",
@@ -175,14 +264,15 @@ class ContentToolRequest(Content):
175
264
  arguments
176
265
  The arguments to pass to the tool/function.
177
266
  tool
178
- The tool/function to be called. This is set internally by chatlas's tool
179
- calling loop.
267
+ Serializable information about the tool. This is set internally by
268
+ chatlas's tool calling loop and contains only the metadata needed
269
+ for serialization (name, description, parameters, annotations).
180
270
  """
181
271
 
182
272
  id: str
183
273
  name: str
184
274
  arguments: object
185
- tool: Optional["Tool"] = None
275
+ tool: Optional[ToolInfo] = None
186
276
 
187
277
  content_type: ContentTypeEnum = "tool_request"
188
278
 
@@ -2,7 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  import warnings
5
- from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Optional
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ AsyncGenerator,
9
+ Awaitable,
10
+ Callable,
11
+ Optional,
12
+ cast,
13
+ )
6
14
 
7
15
  import openai
8
16
  from pydantic import BaseModel, Field, create_model
@@ -12,6 +20,7 @@ from ._content import (
12
20
  ContentToolResult,
13
21
  ContentToolResultImage,
14
22
  ContentToolResultResource,
23
+ ToolAnnotations,
15
24
  )
16
25
 
17
26
  __all__ = (
@@ -22,7 +31,6 @@ __all__ = (
22
31
  if TYPE_CHECKING:
23
32
  from mcp import ClientSession as MCPClientSession
24
33
  from mcp import Tool as MCPTool
25
- from mcp.types import ToolAnnotations
26
34
  from openai.types.chat import ChatCompletionToolParam
27
35
 
28
36
 
@@ -44,8 +52,7 @@ class Tool:
44
52
  parameters
45
53
  A dictionary describing the input parameters and their types.
46
54
  annotations
47
- Additional properties that describe the tool and its behavior. Should be
48
- a `from mcp.types import ToolAnnotations` instance.
55
+ Additional properties that describe the tool and its behavior.
49
56
  """
50
57
 
51
58
  func: Callable[..., Any] | Callable[..., Awaitable[Any]]
@@ -77,6 +84,7 @@ class Tool:
77
84
  cls: type["Tool"],
78
85
  func: Callable[..., Any] | Callable[..., Awaitable[Any]],
79
86
  *,
87
+ name: Optional[str] = None,
80
88
  model: Optional[type[BaseModel]] = None,
81
89
  annotations: "Optional[ToolAnnotations]" = None,
82
90
  ) -> "Tool":
@@ -87,6 +95,9 @@ class Tool:
87
95
  ----------
88
96
  func
89
97
  The function to wrap as a tool.
98
+ name
99
+ The name of the tool. If not provided, the name will be inferred from the
100
+ function's name.
90
101
  model
91
102
  A Pydantic model that describes the input parameters for the function.
92
103
  If not provided, the model will be inferred from the function's type hints.
@@ -94,8 +105,7 @@ class Tool:
94
105
  Note that the name and docstring of the model takes precedence over the
95
106
  name and docstring of the function.
96
107
  annotations
97
- Additional properties that describe the tool and its behavior. Should be
98
- a `from mcp.types import ToolAnnotations` instance.
108
+ Additional properties that describe the tool and its behavior.
99
109
 
100
110
  Returns
101
111
  -------
@@ -114,7 +124,8 @@ class Tool:
114
124
  # Throw if there is a mismatch between the model and the function parameters
115
125
  params = inspect.signature(func).parameters
116
126
  fields = model.model_fields
117
- diff = set(params) ^ set(fields)
127
+ fields_alias = [val.alias if val.alias else key for key, val in fields.items()]
128
+ diff = set(params) ^ set(fields_alias)
118
129
  if diff:
119
130
  raise ValueError(
120
131
  f"`model` fields must match tool function parameters exactly. "
@@ -125,7 +136,7 @@ class Tool:
125
136
 
126
137
  return cls(
127
138
  func=func,
128
- name=model.__name__ or func.__name__,
139
+ name=name or model.__name__ or func.__name__,
129
140
  description=model.__doc__ or func.__doc__ or "",
130
141
  parameters=params,
131
142
  annotations=annotations,
@@ -203,12 +214,17 @@ class Tool:
203
214
 
204
215
  params = mcp_tool_input_schema_to_param_schema(mcp_tool.inputSchema)
205
216
 
217
+ # Convert MCP ToolAnnotations to our TypedDict format
218
+ annotations = None
219
+ if mcp_tool.annotations:
220
+ annotations = cast(ToolAnnotations, mcp_tool.annotations.model_dump())
221
+
206
222
  return cls(
207
223
  func=_utils.wrap_async(_call),
208
224
  name=mcp_tool.name,
209
225
  description=mcp_tool.description or "",
210
226
  parameters=params,
211
- annotations=mcp_tool.annotations,
227
+ annotations=annotations,
212
228
  )
213
229
 
214
230
 
@@ -13,7 +13,7 @@ else:
13
13
  # Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
14
14
  # they should both come from the same typing module.
15
15
  # https://peps.python.org/pep-0655/#usage-in-python-3-11
16
- if sys.version_info >= (3, 11):
16
+ if sys.version_info >= (3, 12):
17
17
  from typing import NotRequired, Required, TypedDict
18
18
  else:
19
19
  from typing_extensions import NotRequired, Required, TypedDict
@@ -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.11.0'
32
- __version_tuple__ = version_tuple = (0, 11, 0)
31
+ __version__ = version = '0.11.1'
32
+ __version_tuple__ = version_tuple = (0, 11, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -13,6 +13,8 @@ from .._content import (
13
13
  ContentToolRequest,
14
14
  ContentToolResult,
15
15
  ImageContentTypes,
16
+ ToolAnnotations,
17
+ ToolInfo,
16
18
  )
17
19
  from .._provider import ModelInfo
18
20
  from .._tokens import TokenUsage
@@ -32,6 +34,8 @@ __all__ = (
32
34
  "ImageContentTypes",
33
35
  "SubmitInputArgsT",
34
36
  "TokenUsage",
37
+ "ToolAnnotations",
38
+ "ToolInfo",
35
39
  "MISSING_TYPE",
36
40
  "MISSING",
37
41
  "ModelInfo",
@@ -188,6 +188,8 @@ quartodoc:
188
188
  - types.MISSING
189
189
  - types.SubmitInputArgsT
190
190
  - types.TokenUsage
191
+ - types.ToolAnnotations
192
+ - types.ToolInfo
191
193
 
192
194
 
193
195
 
@@ -90,11 +90,7 @@ def assert_tools_simple(chat_fun: ChatFun, stream: bool = True):
90
90
 
91
91
 
92
92
  def assert_tools_simple_stream_content(chat_fun: ChatFun):
93
- try:
94
- from mcp.types import ToolAnnotations
95
- except ImportError:
96
- pytest.skip("mcp is not installed")
97
- return
93
+ from chatlas._content import ToolAnnotations
98
94
 
99
95
  chat = chat_fun(system_prompt="Be very terse, not even punctuation.")
100
96
 
@@ -114,7 +110,7 @@ def assert_tools_simple_stream_content(chat_fun: ChatFun):
114
110
  assert request[0].tool is not None
115
111
  assert request[0].tool.name == "get_date"
116
112
  assert request[0].tool.annotations is not None
117
- assert request[0].tool.annotations.title == "Get Date"
113
+ assert request[0].tool.annotations["title"] == "Get Date"
118
114
 
119
115
  # Emits a response (with a reference to the request)
120
116
  response = [x for x in chunks if isinstance(x, ContentToolResult)]
@@ -3,6 +3,7 @@ from typing import Any, Optional, Union
3
3
  import pytest
4
4
 
5
5
  from chatlas import ChatOpenAI
6
+ from chatlas._content import ToolInfo
6
7
  from chatlas.types import ContentToolRequest, ContentToolResult
7
8
 
8
9
 
@@ -113,11 +114,12 @@ def test_invoke_tool_returns_tool_result():
113
114
  name: str = "tool",
114
115
  args: Optional[dict[str, Any]] = None,
115
116
  ):
117
+ tool_obj = chat._tools.get(name)
116
118
  return ContentToolRequest(
117
119
  id="id",
118
120
  name=name,
119
121
  arguments=args or {},
120
- tool=chat._tools.get(name),
122
+ tool=ToolInfo.from_tool(tool_obj) if tool_obj else None,
121
123
  )
122
124
 
123
125
  req1 = new_tool_request()
@@ -178,11 +180,12 @@ async def test_invoke_tool_returns_tool_result_async():
178
180
  name: str = "tool",
179
181
  args: Optional[dict[str, Any]] = None,
180
182
  ):
183
+ tool_obj = chat._tools.get(name)
181
184
  return ContentToolRequest(
182
185
  id="id",
183
186
  name=name,
184
187
  arguments=args or {},
185
- tool=chat._tools.get(name),
188
+ tool=ToolInfo.from_tool(tool_obj) if tool_obj else None,
186
189
  )
187
190
 
188
191
  req1 = new_tool_request()
@@ -254,18 +257,20 @@ def test_tool_custom_result():
254
257
  chat.register_tool(custom_tool)
255
258
  chat.register_tool(custom_tool_err)
256
259
 
260
+ tool_obj = chat._tools.get("custom_tool")
257
261
  req = ContentToolRequest(
258
262
  id="id",
259
263
  name="custom_tool",
260
264
  arguments={},
261
- tool=chat._tools.get("custom_tool"),
265
+ tool=ToolInfo.from_tool(tool_obj) if tool_obj else None,
262
266
  )
263
267
 
268
+ tool_err_obj = chat._tools.get("custom_tool_err")
264
269
  req_err = ContentToolRequest(
265
270
  id="id",
266
271
  name="custom_tool_err",
267
272
  arguments={},
268
- tool=chat._tools.get("custom_tool_err"),
273
+ tool=ToolInfo.from_tool(tool_err_obj) if tool_err_obj else None,
269
274
  )
270
275
 
271
276
  results = list(chat._invoke_tool(req))
@@ -316,18 +321,20 @@ async def test_tool_custom_result_async():
316
321
  chat.register_tool(custom_tool)
317
322
  chat.register_tool(custom_tool_err)
318
323
 
324
+ tool_obj = chat._tools.get("custom_tool")
319
325
  req = ContentToolRequest(
320
326
  id="id",
321
327
  name="custom_tool",
322
328
  arguments={},
323
- tool=chat._tools.get("custom_tool"),
329
+ tool=ToolInfo.from_tool(tool_obj) if tool_obj else None,
324
330
  )
325
331
 
332
+ tool_err_obj = chat._tools.get("custom_tool_err")
326
333
  req_err = ContentToolRequest(
327
334
  id="id",
328
335
  name="custom_tool_err",
329
336
  arguments={},
330
- tool=chat._tools.get("custom_tool_err"),
337
+ tool=ToolInfo.from_tool(tool_err_obj) if tool_err_obj else None,
331
338
  )
332
339
 
333
340
  results = []
@@ -361,3 +368,38 @@ async def test_tool_custom_result_async():
361
368
  assert res_err.name == req_err.name
362
369
  assert res_err.arguments == req_err.arguments
363
370
 
371
+
372
+ def test_content_tool_request_serializable():
373
+ """Test that ContentToolRequest with Tool instance is JSON serializable"""
374
+ chat = ChatOpenAI()
375
+
376
+ def add(x: int, y: int) -> int:
377
+ """Add two numbers"""
378
+ return x + y
379
+
380
+ chat.register_tool(add)
381
+
382
+ # Create a ContentToolRequest with the Tool info
383
+ tool = chat._tools["add"]
384
+ request = ContentToolRequest(
385
+ id="test-123",
386
+ name="add",
387
+ arguments={"x": 1, "y": 2},
388
+ tool=ToolInfo.from_tool(tool),
389
+ )
390
+
391
+ # Test that it can be serialized to JSON
392
+ json_data = request.model_dump_json()
393
+ assert isinstance(json_data, str)
394
+
395
+ # Test that the JSON can be parsed
396
+ parsed = ContentToolRequest.model_validate_json(json_data)
397
+ assert parsed.id == "test-123"
398
+ assert parsed.name == "add"
399
+ assert parsed.arguments == {"x": 1, "y": 2}
400
+ assert parsed.content_type == "tool_request"
401
+
402
+ # Test that the tool is serialized (func is excluded from serialization)
403
+ assert parsed.tool is not None
404
+ assert parsed.tool.name == "add"
405
+ assert parsed.tool.description == "Add two numbers"