From 00ce37b6140af1ff5add6ad5de729343820b01fe Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 10:34:47 -0600 Subject: [PATCH 1/8] fix(pkg-py): allow full client object to read, but not set (after initialization) --- pkg-py/src/querychat/_querychat.py | 44 ++++++++++++++---------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index 3e4f59f5..6179c1c1 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -114,7 +114,7 @@ def __init__( self.id = id or table_name - self.client = normalize_client(client) + self._client = normalize_client(client) if greeting is None: print( @@ -306,7 +306,7 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: data_source=self.data_source, system_prompt=self.system_prompt, greeting=self.greeting, - client=self.client, + client=self._client, enable_bookmarking=enable_bookmarking, ) @@ -416,7 +416,22 @@ def title(self, value: Optional[str] = None) -> str | None | bool: else: return vals.title.set(value) - def generate_greeting(self, *, echo: Literal["none", "text"] = "none"): + def client(self): + """ + Obtain the chat client being used by this QueryChat instance. + + Returns + ------- + : + None + + """ + vals = self._server_values + if vals is None: + raise RuntimeError("Must call .server() before .client()") + return vals.client + + def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): """ Generate a welcome greeting for the chat. @@ -428,7 +443,7 @@ def generate_greeting(self, *, echo: Literal["none", "text"] = "none"): Parameters ---------- echo - If `echo = "text"`, prints the greeting to standard output. If + If `echo = "output"`, prints the greeting to standard output. If `echo = "none"` (default), does not print anything. Returns @@ -437,7 +452,7 @@ def generate_greeting(self, *, echo: Literal["none", "text"] = "none"): The greeting string (in Markdown format). """ - client = copy.deepcopy(self.client) + client = copy.deepcopy(self._client) client.system_prompt = self.system_prompt client.set_turns([]) prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." @@ -510,25 +525,6 @@ def set_data_source( """ self.data_source = normalize_data_source(data_source, table_name) - def set_client(self, client: str | chatlas.Chat) -> None: - """ - Set a new chat client for the QueryChat object. - - Parameters - ---------- - client - A `chatlas.Chat` object or a string to be passed to - `chatlas.ChatAuto()` describing the model to use (e.g. - `"openai/gpt-4.1"`). - - Returns - ------- - : - None - - """ - self.client = normalize_client(client) - class QueryChat(QueryChatBase): def server(self, *, enable_bookmarking: bool = False) -> None: From d99e57f1d6e50a917437264b962feaf48f2b8877 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 10:41:45 -0600 Subject: [PATCH 2/8] fix(pkg-py): make .data_source a read-only property --- pkg-py/src/querychat/_querychat.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index 6179c1c1..d510fc44 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -104,7 +104,7 @@ def __init__( ``` """ - self.data_source = normalize_data_source(data_source, table_name) + self._data_source = normalize_data_source(data_source, table_name) # Validate table name (must begin with letter, contain only letters, numbers, underscores) if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", table_name): @@ -127,7 +127,7 @@ def __init__( self.greeting = greeting.read_text() if isinstance(greeting, Path) else greeting self.system_prompt = get_system_prompt( - self.data_source, + self._data_source, data_description=data_description, extra_instructions=extra_instructions, prompt_template=prompt_template, @@ -160,7 +160,7 @@ def app( """ enable_bookmarking = bookmark_store != "disable" - table_name = self.data_source.table_name + table_name = self._data_source.table_name def app_ui(request): return ui.page_sidebar( @@ -303,7 +303,7 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: # Call the server module self._server_values = mod_server( self.id, - data_source=self.data_source, + data_source=self._data_source, system_prompt=self.system_prompt, greeting=self.greeting, client=self._client, @@ -504,26 +504,18 @@ def set_system_prompt( prompt_template=prompt_template, ) - def set_data_source( - self, data_source: IntoFrame | sqlalchemy.Engine | DataSource, table_name: str - ) -> None: + @property + def data_source(self): """ - Set a new data source for the QueryChat object. - - Parameters - ---------- - data_source - The new data source to use. - table_name - If a data_source is a data frame, a name to use to refer to the table + Get the current data source. Returns ------- : - None + The current data source. """ - self.data_source = normalize_data_source(data_source, table_name) + return self._data_source class QueryChat(QueryChatBase): From c00b9baec5881e7ef4a994e5ee2805935c5c64d3 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 11:00:09 -0600 Subject: [PATCH 3/8] fix(pkg-py): fix setting of system prompt after server-initialization --- pkg-py/src/querychat/_querychat.py | 112 +++++++++++++++-------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index d510fc44..676e5e2d 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -40,6 +40,7 @@ def __init__( greeting: Optional[str | Path] = None, client: Optional[str | chatlas.Chat] = None, data_description: Optional[str | Path] = None, + categorical_threshold: int = 10, extra_instructions: Optional[str | Path] = None, prompt_template: Optional[str | Path] = None, ): @@ -79,6 +80,9 @@ def __init__( Description of the data in plain text or Markdown. If a pathlib.Path object is passed, querychat will read the contents of the path into a string with `.read_text()`. + categorical_threshold + Threshold for determining if a column is categorical based on number of + unique values. extra_instructions Additional instructions for the chat model. If a pathlib.Path object is passed, querychat will read the contents of the path into a string with @@ -126,10 +130,11 @@ def __init__( self.greeting = greeting.read_text() if isinstance(greeting, Path) else greeting - self.system_prompt = get_system_prompt( + self._system_prompt = get_system_prompt( self._data_source, data_description=data_description, extra_instructions=extra_instructions, + categorical_threshold=categorical_threshold, prompt_template=prompt_template, ) @@ -304,7 +309,7 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: self._server_values = mod_server( self.id, data_source=self._data_source, - system_prompt=self.system_prompt, + system_prompt=self._system_prompt, greeting=self.greeting, client=self._client, enable_bookmarking=enable_bookmarking, @@ -416,21 +421,6 @@ def title(self, value: Optional[str] = None) -> str | None | bool: else: return vals.title.set(value) - def client(self): - """ - Obtain the chat client being used by this QueryChat instance. - - Returns - ------- - : - None - - """ - vals = self._server_values - if vals is None: - raise RuntimeError("Must call .server() before .client()") - return vals.client - def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): """ Generate a welcome greeting for the chat. @@ -453,56 +443,68 @@ def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): """ client = copy.deepcopy(self._client) - client.system_prompt = self.system_prompt + client.system_prompt = self._system_prompt client.set_turns([]) prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." return str(client.chat(prompt, echo=echo)) - def set_system_prompt( - self, - data_source: DataSource, - *, - data_description: Optional[str | Path] = None, - extra_instructions: Optional[str | Path] = None, - categorical_threshold: int = 10, - prompt_template: Optional[str | Path] = None, - ) -> None: + @property + def client(self): """ - Customize the system prompt. + Obtain the underlying chat client. - Control the logic behind how the system prompt is generated based on the - data source's schema and optional additional context and instructions. + If called before `.server()`, this returns the base client provided at + initialization. If called after `.server()`, this returns the session-specific + client used for the active Shiny session. - Note - ---- - This method is for parametrized system prompt generation only. To set a - fully custom system prompt string, set the `system_prompt` attribute - directly. + Returns + ------- + : + None + + """ + vals = self._server_values + if vals is None: + return self._client + else: + return vals.client + + @property + def system_prompt(self): + """ + Get the current system prompt. + + Returns + ------- + : + The current system prompt. + + """ + vals = self._server_values + if vals is None: + return self._system_prompt + else: + return vals.client.system_prompt + + @system_prompt.setter + def system_prompt(self, value: str): + """ + Set a new system prompt. Parameters ---------- - data_source - A data source to generate schema information from - data_description - Optional description of the data, in plain text or Markdown format - extra_instructions - Optional additional instructions for the chat model, in plain text or - Markdown format - categorical_threshold - Threshold for determining if a column is categorical based on number of - unique values - prompt_template - Optional `Path` to or string of a custom prompt template. If not provided, the default - querychat template will be used. + value + The new system prompt string. + + Returns + ------- + : + None """ - self.system_prompt = get_system_prompt( - data_source, - data_description=data_description, - extra_instructions=extra_instructions, - categorical_threshold=categorical_threshold, - prompt_template=prompt_template, - ) + self._system_prompt = value + if self._server_values is not None: + self._server_values.client.system_prompt = value @property def data_source(self): From a2d4156061c1687b5a810e7ea3fac27e42a4a6f4 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 11:33:51 -0600 Subject: [PATCH 4/8] Fork chat on init and simplify --- pkg-py/src/querychat/_querychat.py | 55 ++++------------------- pkg-py/src/querychat/_querychat_module.py | 20 +++------ 2 files changed, 14 insertions(+), 61 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index 676e5e2d..c9315064 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -118,8 +118,6 @@ def __init__( self.id = id or table_name - self._client = normalize_client(client) - if greeting is None: print( "Warning: No greeting provided; the LLM will be invoked at conversation start to generate one. " @@ -130,7 +128,7 @@ def __init__( self.greeting = greeting.read_text() if isinstance(greeting, Path) else greeting - self._system_prompt = get_system_prompt( + prompt = get_system_prompt( self._data_source, data_description=data_description, extra_instructions=extra_instructions, @@ -138,6 +136,12 @@ def __init__( prompt_template=prompt_template, ) + client = normalize_client(client) + client2 = copy.deepcopy(client) + client2.set_turns([]) + client2.system_prompt = prompt + self._client = client2 + # Populated when ._server() gets called (in an active session) self._server_values: ModServerResult | None = None @@ -309,7 +313,6 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: self._server_values = mod_server( self.id, data_source=self._data_source, - system_prompt=self._system_prompt, greeting=self.greeting, client=self._client, enable_bookmarking=enable_bookmarking, @@ -443,7 +446,6 @@ def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): """ client = copy.deepcopy(self._client) - client.system_prompt = self._system_prompt client.set_turns([]) prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." return str(client.chat(prompt, echo=echo)) @@ -463,48 +465,7 @@ def client(self): None """ - vals = self._server_values - if vals is None: - return self._client - else: - return vals.client - - @property - def system_prompt(self): - """ - Get the current system prompt. - - Returns - ------- - : - The current system prompt. - - """ - vals = self._server_values - if vals is None: - return self._system_prompt - else: - return vals.client.system_prompt - - @system_prompt.setter - def system_prompt(self, value: str): - """ - Set a new system prompt. - - Parameters - ---------- - value - The new system prompt string. - - Returns - ------- - : - None - - """ - self._system_prompt = value - if self._server_values is not None: - self._server_values.client.system_prompt = value + return self._client @property def data_source(self): diff --git a/pkg-py/src/querychat/_querychat_module.py b/pkg-py/src/querychat/_querychat_module.py index 6153a5b8..02175975 100644 --- a/pkg-py/src/querychat/_querychat_module.py +++ b/pkg-py/src/querychat/_querychat_module.py @@ -1,6 +1,5 @@ from __future__ import annotations -import copy from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Callable, Union @@ -48,7 +47,6 @@ class ModServerResult: df: Callable[[], pd.DataFrame] sql: ReactiveString title: ReactiveStringOrNone - client: chatlas.Chat @module.server @@ -58,7 +56,6 @@ def mod_server( session: Session, *, data_source: DataSource, - system_prompt: str, greeting: str | None, client: chatlas.Chat, enable_bookmarking: bool, @@ -68,20 +65,15 @@ def mod_server( title = ReactiveStringOrNone(None) has_greeted = reactive.value[bool](False) # noqa: FBT003 - # Set up the chat object for this session - chat = copy.deepcopy(client) - chat.set_turns([]) - chat.system_prompt = system_prompt - # Create the tool functions update_dashboard_tool = tool_update_dashboard(data_source, sql, title) reset_dashboard_tool = tool_reset_dashboard(sql, title) query_tool = tool_query(data_source) # Register tools with annotations for the UI - chat.register_tool(update_dashboard_tool) - chat.register_tool(query_tool) - chat.register_tool(reset_dashboard_tool) + client.register_tool(update_dashboard_tool) + client.register_tool(query_tool) + client.register_tool(reset_dashboard_tool) # Execute query when SQL changes @reactive.calc @@ -97,7 +89,7 @@ def filtered_df(): # Handle user input @chat_ui.on_user_submit async def _(user_input: str): - stream = await chat.stream_async(user_input, echo="none", content="all") + stream = await client.stream_async(user_input, echo="none", content="all") await chat_ui.append_message_stream(stream) @reactive.effect @@ -108,7 +100,7 @@ async def greet_on_startup(): if greeting: await chat_ui.append_message(greeting) elif greeting is None: - stream = await chat.stream_async( + stream = await client.stream_async( "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list.", echo="none", ) @@ -153,4 +145,4 @@ def _on_restore(x: RestoreState) -> None: if "querychat_has_greeted" in vals: has_greeted.set(vals["querychat_has_greeted"]) - return ModServerResult(df=filtered_df, sql=sql, title=title, client=chat) + return ModServerResult(df=filtered_df, sql=sql, title=title) From 605ed385bc5202e387dcb1dbb5be7cb6e7402a81 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 11:38:38 -0600 Subject: [PATCH 5/8] Make client an attribute --- pkg-py/src/querychat/_querychat.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index c9315064..f1d49e05 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -140,7 +140,7 @@ def __init__( client2 = copy.deepcopy(client) client2.set_turns([]) client2.system_prompt = prompt - self._client = client2 + self.client = client2 # Populated when ._server() gets called (in an active session) self._server_values: ModServerResult | None = None @@ -314,7 +314,7 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: self.id, data_source=self._data_source, greeting=self.greeting, - client=self._client, + client=self.client, enable_bookmarking=enable_bookmarking, ) @@ -445,28 +445,11 @@ def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): The greeting string (in Markdown format). """ - client = copy.deepcopy(self._client) + client = copy.deepcopy(self.client) client.set_turns([]) prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." return str(client.chat(prompt, echo=echo)) - @property - def client(self): - """ - Obtain the underlying chat client. - - If called before `.server()`, this returns the base client provided at - initialization. If called after `.server()`, this returns the session-specific - client used for the active Shiny session. - - Returns - ------- - : - None - - """ - return self._client - @property def data_source(self): """ From 5b89762244e60aaeea94095b781f8fb20bba90ee Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 11:51:16 -0600 Subject: [PATCH 6/8] Cleanup --- pkg-py/src/querychat/_querychat.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index f1d49e05..0f60e18a 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -137,10 +137,9 @@ def __init__( ) client = normalize_client(client) - client2 = copy.deepcopy(client) - client2.set_turns([]) - client2.system_prompt = prompt - self.client = client2 + self.client = copy.deepcopy(client) + self.client.set_turns([]) + self.client.system_prompt = prompt # Populated when ._server() gets called (in an active session) self._server_values: ModServerResult | None = None From 6406f9b502546af03dbeffeb9c9155a25945f1b4 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 12:09:22 -0600 Subject: [PATCH 7/8] Client needs to be session-specific --- pkg-py/src/querychat/_querychat.py | 27 ++++++++++++++++++----- pkg-py/src/querychat/_querychat_module.py | 17 +++++++++----- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index 0f60e18a..52136587 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -136,10 +136,11 @@ def __init__( prompt_template=prompt_template, ) + # Fork and empty chat now so the per-session forks are fast client = normalize_client(client) - self.client = copy.deepcopy(client) - self.client.set_turns([]) - self.client.system_prompt = prompt + self._client = copy.deepcopy(client) + self._client.set_turns([]) + self._client.system_prompt = prompt # Populated when ._server() gets called (in an active session) self._server_values: ModServerResult | None = None @@ -313,7 +314,7 @@ def _server(self, *, enable_bookmarking: bool = False) -> None: self.id, data_source=self._data_source, greeting=self.greeting, - client=self.client, + client=self._client, enable_bookmarking=enable_bookmarking, ) @@ -444,11 +445,27 @@ def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): The greeting string (in Markdown format). """ - client = copy.deepcopy(self.client) + client = copy.deepcopy(self._client) client.set_turns([]) prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." return str(client.chat(prompt, echo=echo)) + @property + def client(self): + """ + Get the (session-specific) chat client. + + Returns + ------- + : + The current chat client. + + """ + vals = self._server_values + if vals is None: + raise RuntimeError("Must call .server() before accessing .client") + return vals.client + @property def data_source(self): """ diff --git a/pkg-py/src/querychat/_querychat_module.py b/pkg-py/src/querychat/_querychat_module.py index 02175975..4fd83570 100644 --- a/pkg-py/src/querychat/_querychat_module.py +++ b/pkg-py/src/querychat/_querychat_module.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Callable, Union @@ -47,6 +48,7 @@ class ModServerResult: df: Callable[[], pd.DataFrame] sql: ReactiveString title: ReactiveStringOrNone + client: chatlas.Chat @module.server @@ -65,15 +67,18 @@ def mod_server( title = ReactiveStringOrNone(None) has_greeted = reactive.value[bool](False) # noqa: FBT003 + # Set up the chat object for this session + chat = copy.deepcopy(client) + # Create the tool functions update_dashboard_tool = tool_update_dashboard(data_source, sql, title) reset_dashboard_tool = tool_reset_dashboard(sql, title) query_tool = tool_query(data_source) # Register tools with annotations for the UI - client.register_tool(update_dashboard_tool) - client.register_tool(query_tool) - client.register_tool(reset_dashboard_tool) + chat.register_tool(update_dashboard_tool) + chat.register_tool(query_tool) + chat.register_tool(reset_dashboard_tool) # Execute query when SQL changes @reactive.calc @@ -89,7 +94,7 @@ def filtered_df(): # Handle user input @chat_ui.on_user_submit async def _(user_input: str): - stream = await client.stream_async(user_input, echo="none", content="all") + stream = await chat.stream_async(user_input, echo="none", content="all") await chat_ui.append_message_stream(stream) @reactive.effect @@ -100,7 +105,7 @@ async def greet_on_startup(): if greeting: await chat_ui.append_message(greeting) elif greeting is None: - stream = await client.stream_async( + stream = await chat.stream_async( "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list.", echo="none", ) @@ -145,4 +150,4 @@ def _on_restore(x: RestoreState) -> None: if "querychat_has_greeted" in vals: has_greeted.set(vals["querychat_has_greeted"]) - return ModServerResult(df=filtered_df, sql=sql, title=title) + return ModServerResult(df=filtered_df, sql=sql, title=title, client=chat) From d390a83721854cce8aa83c3a1355beb2d3a88afa Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 12:22:05 -0600 Subject: [PATCH 8/8] Update tests --- pkg-py/tests/test_init_with_pandas.py | 6 ------ pkg-py/tests/test_querychat.py | 29 --------------------------- 2 files changed, 35 deletions(-) diff --git a/pkg-py/tests/test_init_with_pandas.py b/pkg-py/tests/test_init_with_pandas.py index 3f94b639..7c182639 100644 --- a/pkg-py/tests/test_init_with_pandas.py +++ b/pkg-py/tests/test_init_with_pandas.py @@ -39,10 +39,6 @@ def test_init_with_pandas_dataframe(): # Verify the result is properly configured assert qc is not None - assert hasattr(qc, "data_source") - assert hasattr(qc, "system_prompt") - assert hasattr(qc, "greeting") - assert hasattr(qc, "client") def test_init_with_narwhals_dataframe(): @@ -66,8 +62,6 @@ def test_init_with_narwhals_dataframe(): # Verify the result is correctly configured assert qc is not None - assert hasattr(qc, "data_source") - assert hasattr(qc, "system_prompt") def test_init_with_narwhals_lazyframe_direct_query(): diff --git a/pkg-py/tests/test_querychat.py b/pkg-py/tests/test_querychat.py index b8267b46..5ffce90b 100644 --- a/pkg-py/tests/test_querychat.py +++ b/pkg-py/tests/test_querychat.py @@ -39,10 +39,6 @@ def test_querychat_init(sample_df): # Verify basic attributes are set assert qc is not None - assert hasattr(qc, "data_source") - assert hasattr(qc, "system_prompt") - assert hasattr(qc, "greeting") - assert hasattr(qc, "client") assert qc.id == "test_table" # Even without server initialization, we should be able to query the data source @@ -66,31 +62,6 @@ def test_querychat_custom_id(sample_df): assert qc.id == "custom_id" -def test_querychat_set_methods(sample_df): - """Test that setter methods work.""" - qc = QueryChat( - data_source=sample_df, - table_name="test_table", - greeting="Hello!", - ) - - # Test set_system_prompt - qc.set_system_prompt( - qc.data_source, - data_description="A test dataset", - ) - assert "test dataset" in qc.system_prompt.lower() - - # Test set_data_source - new_df = pd.DataFrame({"x": [1, 2, 3]}) - qc.set_data_source(new_df, "new_table") - assert qc.data_source is not None - - # Test set_client - qc.set_client("openai/gpt-4o-mini") - assert qc.client is not None - - def test_querychat_core_reactive_access_before_server_raises(sample_df): """Test that accessing reactive properties before .server() raises error.""" qc = QueryChat(