From 4755302288f93d4c44a36792575c0db64f83ae3f Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 11:01:30 -0500 Subject: [PATCH 01/11] fix: rendering of pre-populated messages in the .app() method --- chatlas/_chat.py | 20 ++++++++++++-------- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index 0d97c1bd..0b363719 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -652,19 +652,23 @@ def app( "Install it with `pip install shiny`." ) + try: + from shinychat import message_content + except ImportError: + raise ImportError( + "The `shinychat` package is required for the `browser` method. " + "Install it with `pip install shinychat`." + ) + + messages = [message_content(x) for x in self.get_turns()] + app_ui = ui.page_fillable( - ui.chat_ui("chat"), + ui.chat_ui("chat", messages=messages), fillable_mobile=True, ) def server(input): # noqa: A002 - chat = ui.Chat( - "chat", - messages=[ - {"role": turn.role, "content": turn.text} - for turn in self.get_turns() - ], - ) + chat = ui.Chat("chat") @chat.on_user_submit async def _(user_input: str): diff --git a/pyproject.toml b/pyproject.toml index c53abeb3..1840587f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", + "shinychat>=0.2.1", "openai", "anthropic[bedrock]", "google-genai>=1.14.0", From d5d142af103ce09fe6a87137e51794738c9b14dd Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 11:50:55 -0500 Subject: [PATCH 02/11] Update changelog --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24376a29..6d586e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for submitting multiple chats in one batch. With batch submission, results can take up to 24 hours to complete, but in return you pay ~50% less than usual. For more, see the [reference](https://posit-dev.github.io/chatlas/reference/) for `batch_chat()`, `batch_chat_text()`, `batch_chat_structured()` and `batch_chat_completed()`. (#177) * The `Chat` class gains new `.chat_structured()` (and `.chat_structured_async()`) methods. These methods supersede the now deprecated `.extract_data()` (and `.extract_data_async()`). The only difference is that the new methods return a `BaseModel` instance (instead of a `dict()`), leading to a better type hinting/checking experience. (#175) +### Bug fixes + +* The `.app()` now correctly renders existing turns that contain tool calls. (#179) + ## [0.12.0] - 2025-09-08 ### Breaking changes diff --git a/pyproject.toml b/pyproject.toml index 1840587f..54e4687e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", - "shinychat>=0.2.1", + "shinychat", "openai", "anthropic[bedrock]", "google-genai>=1.14.0", From a1ce265c7b94c37e9772232ac2829e5b15822eca Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 14:06:57 -0500 Subject: [PATCH 03/11] Use shinychat directly --- chatlas/_chat.py | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index 0b363719..31be6099 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -653,7 +653,7 @@ def app( ) try: - from shinychat import message_content + from shinychat import Chat, chat_ui, message_content except ImportError: raise ImportError( "The `shinychat` package is required for the `browser` method. " @@ -663,12 +663,12 @@ def app( messages = [message_content(x) for x in self.get_turns()] app_ui = ui.page_fillable( - ui.chat_ui("chat", messages=messages), + chat_ui("chat", messages=messages), fillable_mobile=True, ) def server(input): # noqa: A002 - chat = ui.Chat("chat") + chat = Chat("chat") @chat.on_user_submit async def _(user_input: str): diff --git a/pyproject.toml b/pyproject.toml index 54e4687e..db737f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", - "shinychat", + "shinychat>=0.2.2", "openai", "anthropic[bedrock]", "google-genai>=1.14.0", From a2b05046626d91700c14891578c8199dd5097683 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 14:14:45 -0500 Subject: [PATCH 04/11] Enable bookmarking --- chatlas/_chat.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index 31be6099..714ae7bf 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -662,14 +662,17 @@ def app( messages = [message_content(x) for x in self.get_turns()] - app_ui = ui.page_fillable( - chat_ui("chat", messages=messages), - fillable_mobile=True, - ) + def app_ui(x): + return ui.page_fillable( + chat_ui("chat", messages=messages), + fillable_mobile=True, + ) def server(input): # noqa: A002 chat = Chat("chat") + chat.enable_bookmarking(self) + @chat.on_user_submit async def _(user_input: str): if stream: @@ -693,7 +696,7 @@ async def _(user_input: str): ) ) - app = App(app_ui, server) + app = App(app_ui, server, bookmark_store="url") def _run_app(): run_app(app, launch_browser=launch_browser, port=port, host=host) From efa78d01124ca89ce6f7e1ce222ae6e233121460 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:22:57 -0500 Subject: [PATCH 05/11] Add tool_result_role parameter; update changelog --- CHANGELOG.md | 7 +++++-- chatlas/_chat.py | 53 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d586e9c..75d12477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for submitting multiple chats in one batch. With batch submission, results can take up to 24 hours to complete, but in return you pay ~50% less than usual. For more, see the [reference](https://posit-dev.github.io/chatlas/reference/) for `batch_chat()`, `batch_chat_text()`, `batch_chat_structured()` and `batch_chat_completed()`. (#177) * The `Chat` class gains new `.chat_structured()` (and `.chat_structured_async()`) methods. These methods supersede the now deprecated `.extract_data()` (and `.extract_data_async()`). The only difference is that the new methods return a `BaseModel` instance (instead of a `dict()`), leading to a better type hinting/checking experience. (#175) +* The `.get_turns()` method gains a `tool_result_role` parameter. Set `tool_result_role="assistant"`, to collect tool result contents, as well as the surrounding assistant turn contents, into a single assistant turn. This is convenient for display purposes and more generally if you want the tool calling loop to be contained in a single turn. (#179) -### Bug fixes +### Improvements -* The `.app()` now correctly renders existing turns that contain tool calls. (#179) +* The `.app()` method now: + * Enables bookmarking by default (i.e., chat session survives page reload). (#179) + * Correctly renders pre-existing turns that contain tool calls. (#179) ## [0.12.0] - 2025-09-08 diff --git a/chatlas/_chat.py b/chatlas/_chat.py index 714ae7bf..944b3457 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -210,6 +210,7 @@ def get_turns( self, *, include_system_prompt: bool = False, + tool_result_role: Literal["assistant", "user"] = "user", ) -> list[Turn[CompletionT]]: """ Get all the turns (i.e., message contents) in the chat. @@ -218,14 +219,49 @@ def get_turns( ---------- include_system_prompt Whether to include the system prompt in the turns. + tool_result_role + The role to assign to tool results in the chat history. By default, + tool results are assigned the "user" role, since they represent + information provided to the assistant. However, in some contexts + (e.g., for display purposes) it may be more appropriate to assign + them the "assistant" role. In the case of "assistant", sequential + turns with the "assistant" role are collapsed into a single turn. """ if not self._turns: return self._turns if not include_system_prompt and self._turns[0].role == "system": - return self._turns[1:] - return self._turns + turns = self._turns[1:] + else: + turns = self._turns + + if tool_result_role == "user": + return turns + + if tool_result_role != "assistant": + raise ValueError( + f"Expected `tool_result_role` to be one of 'user' or 'assistant', not '{tool_result_role}'" + ) + + # If a turn is purely a tool result, change its role + turns2 = copy.deepcopy(turns) + for turn in turns2: + if all(isinstance(c, ContentToolResult) for c in turn.contents): + turn.role = tool_result_role + + # If two consequitive turns have the same role (i.e., assistant), collapse them into one + final_turns: list[Turn[CompletionT]] = [] + for x in turns2: + if not final_turns: + final_turns.append(x) + continue + if x.role != final_turns[-1].role: + final_turns.append(x) + else: + final_turns[-1].contents.extend(x.contents) + + return final_turns def get_last_turn( self, @@ -609,6 +645,7 @@ def app( port: int = 0, host: str = "127.0.0.1", launch_browser: bool = True, + bookmark_store: Literal["url", "server", "disable"] = "url", bg_thread: Optional[bool] = None, echo: Optional[EchoOptions] = None, content: Literal["text", "all"] = "all", @@ -627,6 +664,12 @@ def app( The host to run the app on (the default is "127.0.0.1"). launch_browser Whether to launch a browser window. + bookmark_store + One of the following (default is "url"): + - `"url"`: Store bookmarks in the URL (default). + - `"server"`: Store bookmarks on the server (requires a server-side + storage backend). + - `"disable"`: Disable bookmarking. bg_thread Whether to run the app in a background thread. If `None`, the app will run in a background thread if the current environment is a notebook. @@ -660,7 +703,9 @@ def app( "Install it with `pip install shinychat`." ) - messages = [message_content(x) for x in self.get_turns()] + messages = [ + message_content(x) for x in self.get_turns(tool_result_role="assistant") + ] def app_ui(x): return ui.page_fillable( @@ -696,7 +741,7 @@ async def _(user_input: str): ) ) - app = App(app_ui, server, bookmark_store="url") + app = App(app_ui, server, bookmark_store=bookmark_store) def _run_app(): run_app(app, launch_browser=launch_browser, port=port, host=host) From e7487d2e54b19c52ca095a75adc822be75e00328 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:30:03 -0500 Subject: [PATCH 06/11] Relax version requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index db737f70..54e4687e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", - "shinychat>=0.2.2", + "shinychat", "openai", "anthropic[bedrock]", "google-genai>=1.14.0", From 472de6b9546844ac5448cdd1f75b7b2293715838 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:34:23 -0500 Subject: [PATCH 07/11] Add unit tests --- tests/test_turns.py | 260 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 1 deletion(-) diff --git a/tests/test_turns.py b/tests/test_turns.py index 450f26c1..b633e665 100644 --- a/tests/test_turns.py +++ b/tests/test_turns.py @@ -1,8 +1,9 @@ import pytest from chatlas import ChatAnthropic +from chatlas._content import ToolInfo from chatlas._turn import Turn -from chatlas.types import ContentJson, ContentText +from chatlas.types import ContentJson, ContentText, ContentToolRequest, ContentToolResult def test_system_prompt_applied_correctly(): @@ -90,3 +91,260 @@ def test_can_extract_text_easily(): ], ) assert turn.text == "ABCDEF" + + +def test_get_turns_tool_result_role_default(): + """Test that tool results have default role of 'user'""" + chat = ChatAnthropic() + + # Create mock tool request and result + tool_request = ContentToolRequest( + id="test-123", + name="add", + arguments={"x": 1, "y": 2}, + ) + tool_result = ContentToolResult( + id="test-123", + name="add", + arguments={"x": 1, "y": 2}, + value=3, + request=tool_request + ) + + # Add turns with mixed content + user_turn = Turn("user", "What is 1 + 2?") + assistant_turn = Turn("assistant", [ContentText(text="I'll calculate that."), tool_request]) + tool_result_turn = Turn("user", [tool_result]) + final_assistant_turn = Turn("assistant", "The answer is 3.") + + chat.set_turns([user_turn, assistant_turn, tool_result_turn, final_assistant_turn]) + + # Default behavior should keep tool result turn as "user" + turns = chat.get_turns() + assert len(turns) == 4 + assert turns[2].role == "user" # Tool result turn remains as user + assert all(isinstance(c, ContentToolResult) for c in turns[2].contents) + + +def test_get_turns_tool_result_role_assistant(): + """Test tool_result_role='assistant' changes tool result turn roles""" + chat = ChatAnthropic() + + # Create mock tool request and result + tool_request = ContentToolRequest( + id="test-123", + name="add", + arguments={"x": 1, "y": 2}, + ) + tool_result = ContentToolResult( + id="test-123", + name="add", + arguments={"x": 1, "y": 2}, + value=3, + request=tool_request + ) + + # Add turns with mixed content + user_turn = Turn("user", "What is 1 + 2?") + assistant_turn = Turn("assistant", [ContentText(text="I'll calculate that."), tool_request]) + tool_result_turn = Turn("user", [tool_result]) + final_assistant_turn = Turn("assistant", "The answer is 3.") + + chat.set_turns([user_turn, assistant_turn, tool_result_turn, final_assistant_turn]) + + # With tool_result_role="assistant", tool result turns should change to assistant + turns = chat.get_turns(tool_result_role="assistant") + assert len(turns) == 2 # Should be collapsed due to consecutive assistant turns + assert turns[0].role == "user" + assert turns[1].role == "assistant" # All assistant content collapsed + + # The second turn should now contain content from all assistant turns and tool result + assert len(turns[1].contents) == 4 # Text + tool_request + tool_result + Text + assert isinstance(turns[1].contents[0], ContentText) + assert isinstance(turns[1].contents[1], ContentToolRequest) + assert isinstance(turns[1].contents[2], ContentToolResult) + assert isinstance(turns[1].contents[3], ContentText) + + +def test_get_turns_tool_result_role_collapse_consecutive(): + """Test that consecutive assistant turns are collapsed when tool_result_role='assistant'""" + chat = ChatAnthropic() + + # Create multiple tool results + tool_result1 = ContentToolResult( + id="test-1", + name="add", + arguments={"x": 1, "y": 2}, + value=3, + ) + tool_result2 = ContentToolResult( + id="test-2", + name="multiply", + arguments={"x": 3, "y": 4}, + value=12, + ) + + # Create turns with multiple consecutive assistant turns via tool results + user_turn = Turn("user", "Do some math") + assistant_turn1 = Turn("assistant", "Let me calculate.") + tool_result_turn1 = Turn("user", [tool_result1]) + tool_result_turn2 = Turn("user", [tool_result2]) + assistant_turn2 = Turn("assistant", "Done with calculations.") + + chat.set_turns([user_turn, assistant_turn1, tool_result_turn1, tool_result_turn2, assistant_turn2]) + + # Default behavior - no collapsing + turns_default = chat.get_turns() + assert len(turns_default) == 5 + + # With tool_result_role="assistant" - should collapse consecutive assistant turns + turns_assistant = chat.get_turns(tool_result_role="assistant") + assert len(turns_assistant) == 2 + assert turns_assistant[0].role == "user" + assert turns_assistant[1].role == "assistant" + + # Second turn should have content from all assistant turns and tool results + assert len(turns_assistant[1].contents) == 4 # Text + tool_result1 + tool_result2 + Text + assert isinstance(turns_assistant[1].contents[0], ContentText) + assert isinstance(turns_assistant[1].contents[1], ContentToolResult) + assert isinstance(turns_assistant[1].contents[2], ContentToolResult) + assert isinstance(turns_assistant[1].contents[3], ContentText) + + +def test_get_turns_tool_result_role_mixed_content(): + """Test tool_result_role with turns containing mixed content""" + chat = ChatAnthropic() + + tool_result = ContentToolResult( + id="test-123", + name="weather", + arguments={"city": "Boston"}, + value={"temp": 75, "condition": "sunny"}, + ) + + # Turn with mixed content (text + tool result) + user_turn = Turn("user", "Check the weather") + assistant_turn = Turn("assistant", "I'll check that.") + mixed_turn = Turn("user", [ContentText(text="Here's additional info:"), tool_result]) + final_turn = Turn("assistant", "The weather is nice!") + + chat.set_turns([user_turn, assistant_turn, mixed_turn, final_turn]) + + # Default behavior - turn with mixed content stays as "user" + turns_default = chat.get_turns() + assert len(turns_default) == 4 + assert turns_default[2].role == "user" + + # With tool_result_role="assistant" - turn with mixed content should NOT change role + # because it's not purely tool results + turns_assistant = chat.get_turns(tool_result_role="assistant") + assert len(turns_assistant) == 4 + assert turns_assistant[2].role == "user" # Should remain user due to mixed content + + +def test_get_turns_tool_result_role_purely_tool_results(): + """Test that only turns with purely tool results change role""" + chat = ChatAnthropic() + + tool_result1 = ContentToolResult( + id="test-1", + name="add", + arguments={"x": 1, "y": 2}, + value=3, + ) + tool_result2 = ContentToolResult( + id="test-2", + name="multiply", + arguments={"x": 3, "y": 4}, + value=12, + ) + + # Create different types of turns + user_turn = Turn("user", "Do calculations") + assistant_turn = Turn("assistant", "I'll help.") + pure_tool_turn = Turn("user", [tool_result1, tool_result2]) # Pure tool results + mixed_tool_turn = Turn("user", [ContentText(text="Results:"), tool_result1]) # Mixed content + final_turn = Turn("assistant", "All done.") + + chat.set_turns([user_turn, assistant_turn, pure_tool_turn, mixed_tool_turn, final_turn]) + + # With tool_result_role="assistant" + turns = chat.get_turns(tool_result_role="assistant") + + # Pure tool turn should change role and be collapsed with assistant turns + # Mixed tool turn should remain as user + expected_roles = ["user", "assistant", "user", "assistant"] + actual_roles = [turn.role for turn in turns] + assert actual_roles == expected_roles + + +def test_get_turns_tool_result_role_invalid_value(): + """Test that invalid tool_result_role values raise ValueError""" + chat = ChatAnthropic() + + # Add some content so validation is triggered + user_turn = Turn("user", "Hello") + chat.set_turns([user_turn]) + + with pytest.raises(ValueError, match="Expected `tool_result_role` to be one of 'user' or 'assistant', not 'invalid'"): + chat.get_turns(tool_result_role="invalid") + + +def test_get_turns_tool_result_role_with_system_prompt(): + """Test tool_result_role works correctly with system prompt inclusion""" + chat = ChatAnthropic(system_prompt="You are a helpful assistant.") + + tool_result = ContentToolResult( + id="test-123", + name="calculate", + arguments={"expr": "2+2"}, + value=4, + ) + + user_turn = Turn("user", "Calculate 2+2") + assistant_turn = Turn("assistant", "I'll calculate that.") + tool_result_turn = Turn("user", [tool_result]) + final_turn = Turn("assistant", "The answer is 4.") + + chat.set_turns([user_turn, assistant_turn, tool_result_turn, final_turn]) + + # Test with system prompt included + turns_with_system = chat.get_turns(include_system_prompt=True, tool_result_role="assistant") + assert len(turns_with_system) == 3 # system + user + collapsed assistant turns + assert turns_with_system[0].role == "system" + assert turns_with_system[1].role == "user" + assert turns_with_system[2].role == "assistant" + + # Test without system prompt + turns_without_system = chat.get_turns(include_system_prompt=False, tool_result_role="assistant") + assert len(turns_without_system) == 2 # user + collapsed assistant turns + assert turns_without_system[0].role == "user" + assert turns_without_system[1].role == "assistant" + + +def test_get_turns_tool_result_role_empty_chat(): + """Test tool_result_role with empty chat""" + chat = ChatAnthropic() + + # Empty chat should return empty list regardless of tool_result_role + assert chat.get_turns(tool_result_role="user") == [] + assert chat.get_turns(tool_result_role="assistant") == [] + + +def test_get_turns_tool_result_role_no_tool_results(): + """Test tool_result_role with chat containing no tool results""" + chat = ChatAnthropic() + + user_turn = Turn("user", "Hello") + assistant_turn = Turn("assistant", "Hi there!") + + chat.set_turns([user_turn, assistant_turn]) + + # Should behave identically regardless of tool_result_role when no tool results present + turns_user = chat.get_turns(tool_result_role="user") + turns_assistant = chat.get_turns(tool_result_role="assistant") + + assert len(turns_user) == 2 + assert len(turns_assistant) == 2 + assert turns_user[0].role == turns_assistant[0].role == "user" + assert turns_user[1].role == turns_assistant[1].role == "assistant" From 86d5a398e89c51ee47165c482bda6f7b0b77febd Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:35:55 -0500 Subject: [PATCH 08/11] Fix error message --- chatlas/_chat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index 944b3457..e620272c 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -691,7 +691,7 @@ def app( from shiny import App, run_app, ui except ImportError: raise ImportError( - "The `shiny` package is required for the `browser` method. " + "The `shiny` package is required for the `app()` method. " "Install it with `pip install shiny`." ) @@ -699,7 +699,7 @@ def app( from shinychat import Chat, chat_ui, message_content except ImportError: raise ImportError( - "The `shinychat` package is required for the `browser` method. " + "The `shinychat` package is required for the `app()` method. " "Install it with `pip install shinychat`." ) From 9a16ae455e6180b9e4ead34c00c85301fe7f3ac5 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 10 Sep 2025 15:37:25 -0500 Subject: [PATCH 09/11] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- chatlas/_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index e620272c..a940a1d3 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -250,7 +250,7 @@ def get_turns( if all(isinstance(c, ContentToolResult) for c in turn.contents): turn.role = tool_result_role - # If two consequitive turns have the same role (i.e., assistant), collapse them into one + # If two consecutive turns have the same role (i.e., assistant), collapse them into one final_turns: list[Turn[CompletionT]] = [] for x in turns2: if not final_turns: From 9cd2e852cb8774819d39fe32b9a415840bdaed75 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:47:15 -0500 Subject: [PATCH 10/11] Cleanup docs; add minimal version constraint --- CHANGELOG.md | 2 +- chatlas/_chat.py | 13 +++++++------ pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d12477..414b3378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for submitting multiple chats in one batch. With batch submission, results can take up to 24 hours to complete, but in return you pay ~50% less than usual. For more, see the [reference](https://posit-dev.github.io/chatlas/reference/) for `batch_chat()`, `batch_chat_text()`, `batch_chat_structured()` and `batch_chat_completed()`. (#177) * The `Chat` class gains new `.chat_structured()` (and `.chat_structured_async()`) methods. These methods supersede the now deprecated `.extract_data()` (and `.extract_data_async()`). The only difference is that the new methods return a `BaseModel` instance (instead of a `dict()`), leading to a better type hinting/checking experience. (#175) -* The `.get_turns()` method gains a `tool_result_role` parameter. Set `tool_result_role="assistant"`, to collect tool result contents, as well as the surrounding assistant turn contents, into a single assistant turn. This is convenient for display purposes and more generally if you want the tool calling loop to be contained in a single turn. (#179) +* The `.get_turns()` method gains a `tool_result_role` parameter. Set `tool_result_role="assistant"` to collect tool result content (plus the surrounding assistant turn contents) into a single assistant turn. This is convenient for display purposes and more generally if you want the tool calling loop to be contained in a single turn. (#179) ### Improvements diff --git a/chatlas/_chat.py b/chatlas/_chat.py index a940a1d3..f1f502ba 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -220,12 +220,13 @@ def get_turns( include_system_prompt Whether to include the system prompt in the turns. tool_result_role - The role to assign to tool results in the chat history. By default, - tool results are assigned the "user" role, since they represent - information provided to the assistant. However, in some contexts - (e.g., for display purposes) it may be more appropriate to assign - them the "assistant" role. In the case of "assistant", sequential - turns with the "assistant" role are collapsed into a single turn. + The role to assign to turns containing tool results. By default, + tool results are assigned a role of "user" since they represent + information provided to the assistant. If set to "assistant" tool + result content (plus the surrounding assistant turn contents) is + collected into a single assistant turn. This is convenient for + display purposes and more generally if you want the tool calling + loop to be contained in a single turn. """ if not self._turns: diff --git a/pyproject.toml b/pyproject.toml index 54e4687e..45aa6da4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", - "shinychat", + "shinychat>=0.2.0", "openai", "anthropic[bedrock]", "google-genai>=1.14.0", From 279383aa0c50c5b2b59ad76f0ed2adf1ade64b17 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 10 Sep 2025 15:53:22 -0500 Subject: [PATCH 11/11] Version constraint won't work because of shinychat's constraint on chatlas --- chatlas/_chat.py | 6 +++++- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chatlas/_chat.py b/chatlas/_chat.py index f1f502ba..81669757 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -697,7 +697,11 @@ def app( ) try: - from shinychat import Chat, chat_ui, message_content + from shinychat import ( + Chat, + chat_ui, + message_content, # pyright: ignore[reportAttributeAccessIssue] + ) except ImportError: raise ImportError( "The `shinychat` package is required for the `app()` method. " diff --git a/pyproject.toml b/pyproject.toml index 45aa6da4..54e4687e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ "matplotlib", "Pillow", "shiny", - "shinychat>=0.2.0", + "shinychat", "openai", "anthropic[bedrock]", "google-genai>=1.14.0",