Skip to main content

reflex-assistant-ui

assistant-ui chat components for Reflex, driven entirely from Python state.

The conversation lives in an rx.State: messages are plain dicts, streaming is an async generator that yields tokens, and every user action — send, stop, edit, regenerate, switch branch, pick a conversation — arrives as a normal Reflex event handler. No API routes, no client-side model calls, no Tailwind.

pip install reflex-assistant-ui

Hello world

import reflex as rx
from reflex_assistant_ui import AssistantUIState, assistant_ui


class ChatState(AssistantUIState, rx.State):
    async def _respond(self, history):
        for word in "Hello from Python, one token at a time.".split():
            yield word + " "


def index() -> rx.Component:
    return assistant_ui.chat(
        messages=ChatState.messages,
        head_id=ChatState.head_id,
        is_running=ChatState.is_running,
        on_new=ChatState.handle_new,
        on_edit=ChatState.handle_edit,
        on_reload=ChatState.handle_reload,
        on_cancel=ChatState.handle_cancel,
        on_branch_change=ChatState.handle_branch_change,
        height="100vh",
    )


app = rx.App()
app.add_page(index)

AssistantUIState is a mixin, so it must be combined with rx.State: class ChatState(AssistantUIState, rx.State). Inheriting from it alone gives a class with no state vars.

What you get

Streaming yield a token, the thread updates; the stop button cancels mid-run
Markdown GFM, tables, links, and fenced code with syntax highlighting and a copy button
Reasoning yield {"reasoning": "…"} renders a block that expands while thinking and folds when done
Tool calls yield {"tool_call": …} renders arguments and result in a collapsible card
Branching Editing a message or regenerating a reply creates a sibling; the branch picker walks between them
Conversations A sidebar thread list with create, switch, rename and delete
Attachments Images and text files, via the composer button or drag-and-drop
Feedback Thumbs up/down reported back to Python
Floating mode The same thread inside a launcher popover
Theming Self-contained CSS with light and dark palettes, restyled through CSS variables

Components

from reflex_assistant_ui import assistant_ui
Call Renders
assistant_ui.chat(**props) Provider + thread. The one-call shortcut.
assistant_ui.floating_chat(**props) Provider + launcher button with the thread in a popover.
assistant_ui.provider(*children, **props) The runtime. Every other component must be inside one.
assistant_ui.thread(**props) The chat surface on its own.
assistant_ui.thread_list(**props) The conversation sidebar.
assistant_ui.modal(*children, **props) The launcher popover on its own.

chat() and floating_chat() route keyword arguments by name: runtime props go to the provider, everything else (including styling props such as height) goes to the thread. For a custom layout, compose them yourself:

assistant_ui.provider(
    rx.hstack(
        rx.box(assistant_ui.thread_list(), width="17rem"),
        rx.box(assistant_ui.thread(height="100%"), flex="1"),
        height="100%",
    ),
    messages=ChatState.messages,
    on_new=ChatState.handle_new,
    ...,
)

Provider props

messages, head_id, is_running, is_disabled, is_send_disabled, is_loading, suggestions, thread_list, attachments ("none" | "image" | "text" | "all"), enable_branching, enable_feedback.

Events: on_new, on_edit, on_reload, on_cancel, on_delete, on_add_tool_result, on_feedback, on_branch_change, on_switch_to_thread, on_switch_to_new_thread, on_rename_thread, on_archive_thread, on_unarchive_thread, on_delete_thread.

Thread props

welcome_title, welcome_subtitle, welcome_suggestions, welcome_icon, placeholder, auto_focus, show_welcome, show_action_bar, show_branch_picker, show_feedback, show_avatar, show_attachments, show_follow_up_suggestions, submit_mode ("enter" | "ctrlEnter" | "none"), max_width.

Writing _respond

_respond receives the visible branch as [{"role": ..., "content": ...}], oldest first, with system_prompt prepended when set. Yield any of:

yield "a text delta"                       # or {"text": "…"}
yield {"reasoning": "a thinking delta"}
yield {"tool_call": tool_call_part("search", {"q": "reflex"})}
yield {"part": image_part("https://example.com/chart.png")}

Re-yield a tool_call with the same tool_call_id to fill in its result:

from reflex_assistant_ui import tool_call_part

part = tool_call_part("get_weather", {"city": "Caracas"})
yield {"tool_call": part}
yield {"tool_call": {**part, "result": {"temp_c": 29}}}

Cancellation is cooperative: the consumer stops as soon as the user presses stop, and self._cancel_requested lets a long generator bail out early.

stream_interval (default 0.05) is the minimum gap between websocket pushes. Raise it for very long replies, set it to 0 to push on every token.

Talking to a model

The component does not care how the reply is produced — _respond just yields. The demo uses LangChain, which keeps tools as ordinary Python functions:

from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_ollama import ChatOllama


@tool
def calculate(expression: str) -> dict:
    """Evaluate an arithmetic expression such as '(18 * 7) / 3'."""
    ...


class ChatState(AssistantUIState, rx.State):
    async def _respond(self, history):
        llm = ChatOllama(model="llama3.2", reasoning=True).bind_tools([calculate])
        messages = to_langchain_messages(history)

        gathered = None
        async for chunk in llm.astream(messages):
            gathered = chunk if gathered is None else gathered + chunk

            if reasoning := chunk.additional_kwargs.get("reasoning_content"):
                yield {"reasoning": reasoning}
            if chunk.content:
                yield chunk.content

        for call in gathered.tool_calls:
            part = tool_call_part(call["name"], call["args"], tool_call_id=call["id"])
            yield {"tool_call": part}
            result = await calculate.ainvoke(call["args"])
            yield {"tool_call": {**part, "result": result}}
            # feed the result back and stream the follow-up turn

assistant_ui_demo/assistant_ui_demo/backend.py has the complete loop, including the retry that drops think: true for models without a reasoning mode.

Nothing ties you to LangChain: any client that can stream — an OpenAI SDK call, httpx against your own endpoint, an agent framework — works the same way.

Listing local models

LangChain has no model-discovery API, so the package ships a tiny dependency-free helper for it (it needs only httpx, which Reflex already brings):

from reflex_assistant_ui import ollama

models = await ollama.list_models()      # [] when the server is down

Messages

Messages are dicts, so they serialize into state without any custom types:

{
    "id": "asst-3f2a…",
    "parent_id": "user-91c0…",
    "role": "assistant",
    "content": [{"type": "text", "text": "Hello"}],
    "status": {"type": "complete", "reason": "stop"},
}

parent_id is what makes branching work — a message with two children is a fork, and the branch picker walks between them. The helpers keep the shape consistent:

from reflex_assistant_ui import (
    RUNNING, assistant_message, complete, file_part, image_part,
    reasoning_part, source_part, text_part, tool_call_part, user_message,
)

status applies to assistant messages only: RUNNING while streaming, then complete(), cancelled() or failed(error).

Styling

The package ships one stylesheet, scoped under .aui-root, with no Tailwind dependency. Override the variables to restyle everything:

.aui-root {
  --aui-accent: #7c3aed;
  --aui-accent-fg: #ffffff;
  --aui-radius-xl: 1rem;
  --aui-user-bubble: #ede9fe;
  --aui-thread-max-width: 52rem;
}

Dark mode follows Reflex's Radix .dark class, an explicit [data-theme="dark"], or the OS preference.

Demo

With uv, from the repository root:

uv sync --extra demo               # package (editable) + langchain-core, langchain-ollama
cd assistant_ui_demo
uv run --extra demo reflex run

uv run resolves the project from the root pyproject.toml, so the demo extra has to be requested there too; uv pip install -e . alone does not bring the LangChain dependencies and the demo fails with ModuleNotFoundError: No module named 'langchain_core'.

With pip:

cd assistant_ui_demo
pip install -e ..
pip install -r requirements.txt    # brings langchain-core and langchain-ollama
reflex run

Four pages:

  • / — streaming chat through LangChain's ChatOllama, with a model picker, temperature, tool calling, attachments and feedback. Falls back to a built-in echo bot when Ollama is not running, so the demo always starts.
  • /threads — the same chat plus a conversation sidebar.
  • /floating — the assistant as a popover over an ordinary page.
  • /showcase — a scripted conversation exercising every message part, with no model required.

For the Ollama pages:

ollama serve
ollama pull llama3.2        # or any model you already have

Tool calling needs a model trained for it (llama3.2, qwen2.5, mistral-nemo and friends); with other models the demo simply answers without calling tools.

How it works

useExternalStoreRuntime is assistant-ui's adapter for applications that own their own message state — which is exactly what a Reflex app does. The bridge (assistant_ui_bridge.jsx, shipped with the package and imported by path) converts Python's message dicts into ThreadMessageLike values, hands them to the runtime as a branchable ExportedMessageRepository, and forwards every runtime callback to a Reflex event. Nothing is inlined into the generated pages, and the npm dependencies are pinned:

package version
@assistant-ui/react 0.15.19
@assistant-ui/react-markdown 0.14.15
remark-gfm 4.0.1
react-syntax-highlighter 16.1.1

On the Python side the package itself depends only on reflex and httpx. The demo's LangChain dependencies live in assistant_ui_demo/requirements.txt, not in the package.

Every state delta rebuilds the message repository, which is O(n) in the number of messages. That is unnoticeable for ordinary conversations; for very long ones raise stream_interval so fewer deltas cross the wire.

Requirements

Python 3.11+, Reflex 0.10.0+ (Reflex 0.10 itself requires Python 3.11 or newer).

Development

uv sync --extra dev --extra demo
uv run pytest
uvx ruff check . && uvx ruff format --check custom_components assistant_ui_demo tests

GitHub Actions run on every push and pull request to develop and main:

  • Quality — ruff lint and format, pytest on Python 3.11–3.13, sdist/wheel build with twine check, and a production build of the demo app.
  • Security — gitleaks, bandit, semgrep, pip-audit, dependency review and CodeQL (Python and JavaScript), plus a weekly scheduled run.

Releasing

  1. Bump the version in pyproject.toml and in custom_components/reflex_assistant_ui/__init__.py, merge to main.

  2. Tag the merge commit and push the tag:

    git tag v0.2.0 && git push origin v0.2.0
    

The Release workflow re-runs quality and security checks, verifies that the tag is on main and matches both versions, builds the distribution, publishes it to PyPI through Trusted Publishing and creates the GitHub release with the files attached.

License

Apache-2.0. assistant-ui is MIT-licensed by AgentbaseAI Inc.

Metadata

Release files for reflex-assistant-ui 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reflex-assistant-ui 0.2.0
File Size Uploaded Provenance
reflex_assistant_ui-0.2.0.tar.gz 41.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for reflex-assistant-ui 0.2.0
File Interpreter ABI Platform
reflex_assistant_ui-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 80.9 kB

Release files / reflex_assistant_ui-0.2.0.tar.gz

Download URL reflex_assistant_ui-0.2.0.tar.gz
Size 41.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f2c1952da7e5c03ed577b858f758b7c1eb7e85445cd5d7ce77cfea26ce32643c
BLAKE2b-256 checksum
How to use checksums
e139d411996d617c6d7883f7fb00edee611823007d6998e78436b4aeb8b20512
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / reflex_assistant_ui-0.2.0-py3-none-any.whl

Download URL reflex_assistant_ui-0.2.0-py3-none-any.whl
Size 39.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9c232c1ffdced5cc8d2e39fd0633cc80e073d03f8132b404d501187d9a64537e
BLAKE2b-256 checksum
How to use checksums
1514b6313e29794bd37bdf458814ca6ce5e0309fa3f771888c4ff2ed71a28bad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for reflex-assistant-ui 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for reflex_assistant_ui-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page