From fa609ff73b399b70e0dde45798598ecdcad1e44f Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 8 Sep 2025 15:46:00 -0500 Subject: [PATCH 1/2] Follow up to #107: pick up tool annotation icon and never escape icon or html display --- .../src/shinychat/_chat_normalize_chatlas.py | 12 ++- pkg-py/tests/playwright/tools/basic/app.py | 81 ++++++++----------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/pkg-py/src/shinychat/_chat_normalize_chatlas.py b/pkg-py/src/shinychat/_chat_normalize_chatlas.py index 991060f7..4f77cfc7 100644 --- a/pkg-py/src/shinychat/_chat_normalize_chatlas.py +++ b/pkg-py/src/shinychat/_chat_normalize_chatlas.py @@ -288,8 +288,18 @@ def tool_result_contents(x: "ContentToolResult") -> Tagifiable: tool = x.request.tool tool_title = None + icon = None if tool and tool.annotations: tool_title = tool.annotations.get("title") + icon = tool.annotations.get("extras", {}).get("icon") + icon = icon or tool.annotations.get("icon") + + # Icon strings and HTML display never get escaped + icon = display.icon or icon + if icon and isinstance(icon, str): + icon = HTML(icon) + if value_type == "html" and isinstance(value, str): + value = HTML(value) # display (tool *result* level) takes precedence over # annotations (tool *definition* level) @@ -301,7 +311,7 @@ def tool_result_contents(x: "ContentToolResult") -> Tagifiable: status="success" if x.error is None else "error", value=value, value_type=value_type, - icon=display.icon, + icon=icon, intent=intent, show_request=display.show_request, expanded=display.open, diff --git a/pkg-py/tests/playwright/tools/basic/app.py b/pkg-py/tests/playwright/tools/basic/app.py index b8e7a433..d5d78899 100644 --- a/pkg-py/tests/playwright/tools/basic/app.py +++ b/pkg-py/tests/playwright/tools/basic/app.py @@ -6,9 +6,9 @@ import faicons from chatlas import ChatAuto, ContentToolResult from chatlas.types import ToolAnnotations -from pydantic import BaseModel, Field from shiny import reactive from shiny.express import input, ui + from shinychat.express import Chat from shinychat.types import ToolResultDisplay @@ -39,20 +39,6 @@ def list_files_impl(): ) -class ListFileParams(BaseModel): - """ - List files in the user's current directory. Always check again when asked. - """ - - path: str = Field(..., description="The path to list files from") - - -class ListFileParamsWithIntent(ListFileParams): - intent: str = Field( - ..., description="The user's intent for this tool", alias="_intent" - ) - - annotations: ToolAnnotations = {} if TOOL_OPTS["with_title"]: annotations["title"] = "List Files" @@ -61,56 +47,59 @@ class ListFileParamsWithIntent(ListFileParams): if TOOL_OPTS["async"]: if TOOL_OPTS["with_intent"]: - async def list_files_func1(path: str, _intent: str): + async def list_files(path: str, _intent: str): # pyright: ignore[reportRedeclaration] + """ + List files in the user's current directory. Always check again when asked. + + Parameters + ---------- + path + The path to list files from. + _intent + Reason for the request to explain the tool call to the user. + """ await asyncio.sleep(random.uniform(1, 10)) return list_files_impl() - chat_client.register_tool( - list_files_func1, - name="list_files", - model=ListFileParamsWithIntent, - annotations=annotations, - ) - else: - async def list_files_func2(path: str): + async def list_files(path: str): # pyright: ignore[reportRedeclaration] + """ + List files in the user's current directory. Always check again when asked. + """ await asyncio.sleep(random.uniform(1, 10)) return list_files_impl() - chat_client.register_tool( - list_files_func2, - name="list_files", - model=ListFileParams, - annotations=annotations, - ) - else: if TOOL_OPTS["with_intent"]: - def list_files_func3(path: str, _intent: str): + def list_files(path: str, _intent: str): # pyright: ignore[reportRedeclaration] + """ + List files in the user's current directory. Always check again when asked. + + Parameters + ---------- + path + The path to list files from. + _intent + Reason for the request to explain the tool call to the user. + """ time.sleep(random.uniform(1, 3)) return list_files_impl() - chat_client.register_tool( - list_files_func3, - name="list_files", - model=ListFileParamsWithIntent, - annotations=annotations, - ) - else: - def list_files_func4(path: str): + def list_files(path: str): # pyright: ignore[reportRedeclaration] + """ + List files in the user's current directory. Always check again when asked. + """ time.sleep(random.uniform(1, 3)) return list_files_impl() - chat_client.register_tool( - list_files_func4, - name="list_files", - model=ListFileParams, - annotations=annotations, - ) +chat_client.register_tool( + list_files, + annotations=annotations, +) ui.page_opts(fillable=True) From d3e6cda152cff659c2f3138013d007a5a8b38aa0 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 9 Sep 2025 10:45:53 -0400 Subject: [PATCH 2/2] chore: make py-format --- pkg-py/src/shinychat/_markdown_stream.py | 14 +++++++++----- pkg-py/tests/playwright/tools/basic/app.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg-py/src/shinychat/_markdown_stream.py b/pkg-py/src/shinychat/_markdown_stream.py index a8cbdc17..4c2f778a 100644 --- a/pkg-py/src/shinychat/_markdown_stream.py +++ b/pkg-py/src/shinychat/_markdown_stream.py @@ -92,9 +92,9 @@ def __init__( async def _mock_task() -> str: return "" - self._latest_stream: reactive.Value[reactive.ExtendedTask[[], str]] = ( - reactive.Value(_mock_task) - ) + self._latest_stream: reactive.Value[ + reactive.ExtendedTask[[], str] + ] = reactive.Value(_mock_task) async def stream( self, @@ -149,7 +149,9 @@ async def _task(): ui = self._session._process_ui(x) result += ui["html"] - await self._send_content_message(ui["html"], "append", ui["deps"]) + await self._send_content_message( + ui["html"], "append", ui["deps"] + ) return result @@ -249,7 +251,9 @@ async def _send_custom_message( ): if self._session.is_stub_session(): return - await self._session.send_custom_message("shinyMarkdownStreamMessage", {**msg}) + await self._session.send_custom_message( + "shinyMarkdownStreamMessage", {**msg} + ) async def _raise_exception(self, e: BaseException): if self.on_error == "unhandled": diff --git a/pkg-py/tests/playwright/tools/basic/app.py b/pkg-py/tests/playwright/tools/basic/app.py index d5d78899..5bcfdcdc 100644 --- a/pkg-py/tests/playwright/tools/basic/app.py +++ b/pkg-py/tests/playwright/tools/basic/app.py @@ -8,7 +8,6 @@ from chatlas.types import ToolAnnotations from shiny import reactive from shiny.express import input, ui - from shinychat.express import Chat from shinychat.types import ToolResultDisplay @@ -96,6 +95,7 @@ def list_files(path: str): # pyright: ignore[reportRedeclaration] time.sleep(random.uniform(1, 3)) return list_files_impl() + chat_client.register_tool( list_files, annotations=annotations,