Skip to content

feat: Allow deferred chat client initialization#207

Merged
cpsievert merged 38 commits intomainfrom
cpsievert/issue-205-deferred-client
Apr 16, 2026
Merged

feat: Allow deferred chat client initialization#207
cpsievert merged 38 commits intomainfrom
cpsievert/issue-205-deferred-client

Conversation

@cpsievert
Copy link
Copy Markdown
Contributor

@cpsievert cpsievert commented Feb 3, 2026

Summary

QueryChat now supports deferred client creation in both Python and R. The chat client (chatlas Chat / ellmer Chat) no longer needs to exist at construction time — you can pass it later via server(client=...), scoped to the current Shiny session.

This unlocks Posit Connect managed OAuth workflows where credentials are only available inside the Shiny server function.

# Python — no API key needed at construction
qc = QueryChat(None, "mtcars", greeting=greeting)

def server(input, output, session):
    qc.server(client=ChatAnthropic(), data_source=mtcars)
# R — same pattern
qc <- QueryChat$new(NULL, "mtcars", greeting = greeting)

server <- function(input, output, session) {
  qc$server(client = chat_anthropic(), data_source = mtcars)
}

Under the hood

  • Store the client "spec" (str | Chat | NULL) instead of eagerly resolving it at init time
  • Add _create_session_client() / create_session_client() as the single resolution point that turns a spec into a fully-configured Chat with system prompt and tools
  • server(client=...) captures the override in a session-local closure so it never mutates the shared spec — safe for concurrent sessions
  • Clone the resolved Chat on every call to prevent mutation of a shared Chat object

Closes #205

Test plan

  • Deferred init: QueryChat(NULL, ...) succeeds without API key
  • Guard rails: $client(), $console(), $generate_greeting() error when data_source is still NULL
  • Deferred then resolved: set data_source later, everything works
  • Session isolation: server(client=...) override doesn't leak to other sessions
  • Clone safety: multiple $client() calls with a Chat spec return independent instances
  • Backward compat: existing patterns with immediate data_source continue to work
  • Full test suites pass (Python and R)

🤖 Generated with Claude Code

When using Posit Connect managed OAuth credentials, chat client
connections need access to HTTP headers in the Shiny session object,
requiring creation inside the server function.

Changes:
- Defer client initialization when data_source=None and client=None
- Add chat_client property getter/setter for setting client after init
- Add client parameter to server() method for deferred pattern
- Add _require_client() method for runtime checks
- Update methods (client, console, generate_greeting) to require client

Closes #205

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
cpsievert and others added 3 commits April 15, 2026 17:55
Client is no longer eagerly normalized at __init__ when not explicitly
provided. Instead, _require_client(default=) resolves the client lazily:
self._client takes priority, then the caller-provided default, then
raises. Methods like .app() and .client() pass default=None (global
default), while .server() passes the user's explicit arg (MISSING if
omitted, which errors).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
normalize_client now deepcopies and clears turns internally, so callers
no longer need to repeat the copy.deepcopy + set_turns([]) sequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpsievert cpsievert requested a review from Copilot April 15, 2026 23:52
@cpsievert cpsievert marked this pull request as draft April 15, 2026 23:53
cpsievert and others added 3 commits April 15, 2026 18:53
Better reflects that the function always returns a new, independent Chat
instance (deepcopied, with turns cleared) rather than just coercing a
value into canonical form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Better reflects that the method resolves and stores the client (side
effect) rather than just validating it exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…greeting()

Eliminates remaining inline copy.deepcopy + set_turns([]) sequences
by reusing create_client, which already handles that.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This comment was marked as resolved.

cpsievert and others added 3 commits April 16, 2026 09:38
Store the client specification without eager resolution. A single
_create_session_client() method resolves spec -> fresh Chat -> system
prompt -> tools at point-of-use.

- Replace dual-role self._client with self._client_spec (just stores spec)
- Delete _ensure_client, replace with _create_session_client
- Rename chat_client property to client_spec (no naming collision with client())
- System prompt assigned in exactly one place (_create_session_client)
- Remove MISSING sentinel from server() signature
- Pass bound method to mod_server instead of lambda/bound-method-as-callable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users calling client() now see "data_source must be set before calling
client()" instead of a reference to the internal _create_session_client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpsievert cpsievert force-pushed the cpsievert/issue-205-deferred-client branch from 74a04d2 to 4148df2 Compare April 16, 2026 14:56
cpsievert and others added 14 commits April 16, 2026 10:09
Keep named helper functions from main for readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove TestRequireClientSpec class and two tests that asserted None is
an invalid client argument, since _require_client_spec no longer exists
and None now resolves to the QUERYCHAT_CLIENT env var or "openai".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cpsievert and others added 10 commits April 16, 2026 12:30
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…session_client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store the client spec (NULL/string/Chat) lazily instead of eagerly
resolving it in initialize(). Add private create_session_client() as
the single resolution point, and delegate $client() and
$generate_greeting() to it.

This enables constructing QueryChat with NULL data_source and no API
key, deferring all resolution until the client is actually needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The client parameter allows per-session chat client overrides without
mutating the shared client spec. This enables Posit Connect managed
OAuth credentials that require session scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the client spec is a Chat object, as_querychat_client() returns
it as-is. Without cloning, subsequent calls to $client() would mutate
the same object (duplicate tool registrations, shared turn history).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpsievert cpsievert changed the title feat(py): Allow deferred chat client initialization feat: Allow deferred chat client initialization Apr 16, 2026
@cpsievert cpsievert marked this pull request as ready for review April 16, 2026 18:39
@cpsievert cpsievert requested a review from Copilot April 16, 2026 18:39
cpsievert and others added 2 commits April 16, 2026 13:39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This comment was marked as resolved.

@cpsievert cpsievert merged commit 1667062 into main Apr 16, 2026
18 of 19 checks passed
@cpsievert cpsievert deleted the cpsievert/issue-205-deferred-client branch April 16, 2026 21:29
cpsievert added a commit that referenced this pull request Apr 16, 2026
Integrate main's deferred client initialization (#207) and narwhals
version constraint (#218) with the ggsql visualization feature branch.
Extended _create_session_client with visualize_query parameter and
updated stub session test to match eager client construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(py) Allow QueryChat class to be created with an empty chat client

2 participants