GUI replies#

A GUI reply is a GUI component the agent sends as a message in the conversation. The client receives it and renders it inline, just like a text message — but instead of plain text, the user sees a form, a chart, a data table, or any other BESSER BUML view element.

This is useful when the conversation reaches a point where structured input or a rich visual is the best way to interact with the user.

Example of a form rendered as a GUI reply in the chat

A form rendered inline as a GUI reply inside the chat conversation.#

AgentGUI#

The AgentGUI class is BAF’s wrapper around a BESSER BUML GUIModel. It adds a few extra attributes that control how the GUI behaves when sent as a reply.

from baf.core.gui.agent_gui import AgentGUI

agent_gui = AgentGUI(
    model,            # a besser GUIModel instance
    gui_id='my_form', # optional; auto-generated UUID if omitted
    persist=True,     # track input changes in the session (see below)
    width='480px',    # optional CSS width for the chat bubble
)

The key attributes of AgentGUI are:

  • model — the underlying GUIModel built with the BESSER BUML library.

  • id — a unique string identifier for this GUI component. Used to distinguish between multiple GUI replies in the same session, and to match form submissions to the right AgentGUI.

  • persist — when True, input changes and form submissions are automatically tracked in session.gui_inputs (see Persisting form inputs below).

  • width — a CSS width value (e.g. '480px', '60%') that overrides the default chat bubble width. Use wider values for multi-column layouts or charts, and narrower ones for simple forms.

Sending a GUI reply#

Use reply_gui() from a state body to send a GUI to the user:

def show_form_body(session: Session):
    websocket_platform.reply_gui(session, registration_gui)

The GUI model is serialised to JSON and sent to the client, which renders it in the chat thread. From that moment, any interaction with the GUI elements (typing in a field, clicking a checkbox, submitting a form) generates GUIEvent events that are dispatched back to the agent.

Building a GUIModel#

GUIModel objects are built with the BESSER BUML library. Below is a minimal example that creates a two-field registration form:

from besser.BUML.metamodel.gui import (
    GUIModel, Module, Screen,
    Form, InputField, InputFieldType,
)
from baf.core.gui.agent_gui import AgentGUI

# Define the form fields
name_field = InputField(
    name="full_name",
    description='Text input field',
    field_type=InputFieldType.Text,
    label="Full Name",
    required=True,
)
email_field = InputField(
    name="email",
    description='Email input field',
    field_type=InputFieldType.Email,
    label="Email",
    required=True,
)

# Group them in a Form component
form = Form(
    name="registration_form",
    description='Form for registration',
    fields=[name_field, email_field],
    inputFields={name_field, email_field},
    submit_label="Register",
)

# Assemble the screen and model
screen = Screen(name="home", view_elements={form}, is_main_page=True, description='Home')
module = Module(name="main", screens={screen})
gui_model = GUIModel(
    name="RegistrationUI",
    description='aaa',
    package="",
    versionCode="1.0",
    versionName="1.0",
    modules={module},
)

# Wrap in AgentGUI
registration_gui = AgentGUI(gui_model, gui_id="registration_form", persist=True)

Note

The BESSER BUML library supports a wide range of view elements: text, buttons, images, links, charts, tables, metric cards, and many input field types (text, dropdown, checkbox group, rating, slider, date range, and more). Refer to the BESSER documentation for the full reference.

Persisting form inputs#

When persist=True is set on an AgentGUI, BAF automatically keeps track of every input change and form submission for that GUI in session.gui_inputs.

session.gui_inputs is a dictionary keyed by GUI id. Each entry is itself a dictionary of {field_name: value} pairs that reflect the latest known state of every form field:

# After the user has interacted with the form, gui_inputs holds the latest values
def process_registration_body(session: Session):
    data = session.gui_inputs.get('registration_form', {})
    name  = data.get('full_name', '')
    email = data.get('email', '')
    session.reply(f'Thanks, {name}! We will reach out at {email}.')

The dictionary is updated in two ways:

  • On field change (onChange events): the individual field’s value is updated.

  • On form submission (onSubmit events): all field values from the submission are merged in at once.

Note

session.gui_inputs is only populated for GUI replies that have persist=True. If persist=False (the default), input events are still dispatched to the agent as regular GUIEvent objects, but their values are not automatically stored.

Reacting to GUI events#

Every interaction with a GUI component — changing a field, clicking a button, submitting a form — generates a GUIEvent that the agent receives.

You can react to any GUI event using a generic when_event transition:

from baf.library.transition.events.base_events import GUIEvent

state1.when_event(GUIEvent()).go_to(state2)

For form submissions specifically, BAF provides the dedicated when_form_submitted() transition. It uses a FormSubmitMatcher condition and is triggered only when an onSubmit event is received. Optionally, you can restrict it to submissions from a specific form by passing the GUI’s id:

# Triggered by any form submission
state1.when_form_submitted().go_to(process_state)

# Triggered only when registration_gui is submitted
state1.when_form_submitted(form_id=registration_gui.id).go_to(process_state)

See Transitions for a full description of when_form_submitted and the other available transitions.

Controlling the bubble width#

By default, GUI replies are displayed at the same width as regular chat messages (up to 78 % of the chat area). For wider layouts — two-column forms, charts, tables — set the width attribute on the AgentGUI:

# Fixed pixel width
wide_gui = AgentGUI(model, width='720px')

# Percentage of the chat area width
half_gui  = AgentGUI(model, width='50%')

The width can be any valid CSS length value. The chat bubble expands to the given size, and the GUI content fills it. If no width is set, the bubble follows the standard chat message sizing rules.

Full example#

Putting it all together: an agent that shows a registration form, waits for the user to submit it, and then confirms the submission.

from baf.core.agent import Agent
from baf.core.gui.agent_gui import AgentGUI
from baf.core.session import Session

agent = Agent('registration_agent')
websocket_platform = agent.use_websocket_platform(use_ui=True)

show_form_state    = agent.new_state('show_form',    initial=True)
process_form_state = agent.new_state('process_form')
done_state         = agent.new_state('done')

# Build and wrap the GUI (see "Building a GUIModel" above)
registration_gui = AgentGUI(model, gui_id='reg_form', persist=True, width='480px')

def show_form_body(session: Session):
    session.reply('Please fill in the registration form below:')
    websocket_platform.reply_gui(session, registration_gui)

show_form_state.set_body(show_form_body)
show_form_state.when_form_submitted(form_id='reg_form').go_to(process_form_state)

def process_form_body(session: Session):
    data  = session.gui_inputs.get('reg_form', {})
    name  = data.get('full_name', '(unknown)')
    email = data.get('email', '(unknown)')
    session.reply(f'Registration received! Name: {name}, Email: {email}.')

process_form_state.set_body(process_form_body)
process_form_state.go_to(done_state)

def done_body(session: Session):
    session.reply('All done!')

done_state.set_body(done_body)

if __name__ == '__main__':
    agent.run()

Generating a GUI reply with an LLM#

Instead of constructing a GUIModel manually, you can describe the desired GUI in plain text and let an LLM build it for you. create_gui_with_llm() instructs the LLM to produce a JSON object conforming to the AgentGUI schema, then deserializes it into a ready-to-send AgentGUI. If the first response is invalid, the error is fed back to the LLM and the call is retried up to max_retries times (default 2). On final failure it logs an error and returns None.

from baf.core.gui.gui_llm import create_gui_with_llm

def answer_body(session: Session):
    gui = create_gui_with_llm(
        llm=gpt,
        prompt=session.event.message,
    )
    if gui is not None:
        websocket_platform.reply_gui(session, gui)
    else:
        session.reply("Sorry, I could not generate that UI.")

Pass gui_id to assign a stable identifier (useful for form-submission matching) and max_retries to control how many correction attempts are made:

gui = create_gui_with_llm(
    llm=gpt,
    prompt="A registration form with name, email and a Submit button.",
    gui_id="reg_form",
    max_retries=3,
)

API References#