Overview
Enable AgentBot/ToolBot to return multiple separate chat messages across different UI frameworks (Marimo, Panel, Streamlit), allowing for rich responses with plots, dataframes, and text as separate messages.
Problem
Currently, chat responses are limited to a single message that can only contain one type of content. Users want to see:
- "Thinking..." message
- "Executing code..." message
- Plot result message
- DataFrame result message
- Final summary message
Architecture Decision
Use framework-agnostic design with streaming/callback pattern where AgentBot yields messages during its ReAct loop instead of returning a single response at the end.
Framework-agnostic design: Start with Marimo as reference implementation, then create adapters for Panel and Streamlit.
Implementation Approach
1. Create Framework-Agnostic Message System
Create llamabot/components/multi_message.py:
ChatMessage dataclass for consistent message structure
MessageRenderer abstract base class for framework-specific rendering
- Framework-specific renderers:
MarimoRenderer, PanelRenderer, StreamlitRenderer
2. Create Framework-Agnostic Chat Interface
Create llamabot/components/chat_interface.py:
MultiMessageChat class with unified API across frameworks
- Methods:
add_text(), add_plot(), add_dataframe(), add_code()
- Consistent interface regardless of UI framework
3. Update AgentBot with Streaming Support
Modify llamabot/bot/agentbot.py:
- Add
on_message_callback parameter to __init__
- During ReAct loop, call callback with intermediate messages:
- After thought phase: callback with thought text
- After tool execution: callback with tool results
- At final response: callback with final answer
- Callback signature:
callback(message: ChatMessage) -> None
4. Framework-Specific Implementations
Marimo Implementation
def chat_turn(messages, config):
"""Generator that yields multiple messages."""
user_message = messages[-1].content
# Create chat interface
chat = MultiMessageChat(MarimoRenderer(mo))
def message_callback(msg: ChatMessage):
yield chat.add_message(msg)
# Use AgentBot with callback
agent = lmb.AgentBot(
tools=[write_and_execute_code(globals())],
on_message_callback=message_callback,
model_name="gpt-4.1",
)
try:
final_result = agent(user_message, describe_dataframes_in_globals(globals()))
yield chat.add_text(final_result.content)
except Exception as e:
yield chat.add_text(f"Error: {str(e)}", role="system")
Panel Implementation
import panel as pn
class PanelChatInterface:
def __init__(self):
self.chat = MultiMessageChat(PanelRenderer())
self.chat_area = pn.Column()
self.input_area = pn.Row()
def add_message(self, message: ChatMessage):
rendered = self.chat.add_message(message)
self.chat_area.append(rendered)
return rendered
def send_message(self, content: str):
# Add user message
self.add_message(ChatMessage(role="user", content=content, message_type="text"))
# Process with AgentBot
agent = lmb.AgentBot(
tools=[write_and_execute_code(globals())],
on_message_callback=self.add_message,
)
try:
result = agent(content, describe_dataframes_in_globals(globals()))
self.add_message(ChatMessage(role="assistant", content=result.content, message_type="text"))
except Exception as e:
self.add_message(ChatMessage(role="system", content=f"Error: {str(e)}", message_type="error"))
Streamlit Implementation
import streamlit as st
class StreamlitChatInterface:
def __init__(self):
self.chat = MultiMessageChat(StreamlitRenderer())
def add_message(self, message: ChatMessage):
with st.chat_message(message.role):
if message.message_type == "text":
st.markdown(message.content)
elif message.message_type == "plot":
st.pyplot(message.content)
elif message.message_type == "dataframe":
st.dataframe(message.content)
elif message.message_type == "code":
st.code(message.content["code"], language="python")
if message.content["result"]:
self.chat.renderer.render_artifact(message.content["result"])
def send_message(self, content: str):
# Add user message
self.add_message(ChatMessage(role="user", content=content, message_type="text"))
# Process with AgentBot
agent = lmb.AgentBot(
tools=[write_and_execute_code(globals())],
on_message_callback=self.add_message,
)
try:
result = agent(content, describe_dataframes_in_globals(globals()))
self.add_message(ChatMessage(role="assistant", content=result.content, message_type="text"))
except Exception as e:
self.add_message(ChatMessage(role="system", content=f"Error: {str(e)}", message_type="error"))
Files to Create/Modify
Core Framework-Agnostic Components
- Create:
llamabot/components/multi_message.py (ChatMessage, MessageRenderer, framework renderers)
- Create:
llamabot/components/chat_interface.py (MultiMessageChat class)
- Create:
tests/components/test_multi_message.py
- Create:
tests/components/test_chat_interface.py
Framework-Specific Implementations
- Create:
llamabot/components/marimo_chat.py (Marimo-specific chat interface)
- Create:
llamabot/components/panel_chat.py (Panel-specific chat interface)
- Create:
llamabot/components/streamlit_chat.py (Streamlit-specific chat interface)
AgentBot Updates
- Modify:
llamabot/bot/agentbot.py (add callback support)
- Create:
tests/bot/test_agentbot_streaming.py
Example Notebooks
- Modify:
notebooks/toolbot_chatdata.py (use new framework-agnostic interface)
- Create:
examples/panel_multi_message_chat.py
- Create:
examples/streamlit_multi_message_chat.py
Key Design Decisions
- Framework-agnostic core:
ChatMessage and MessageRenderer abstract the differences
- Pluggable renderers: Each framework has its own renderer implementation
- Unified interface:
MultiMessageChat provides consistent API across frameworks
- Callback pattern: AgentBot calls back with
ChatMessage objects
- Progressive enhancement: Start with Marimo, add Panel/Streamlit later
Benefits
- Cross-framework compatibility: Same code works with Marimo, Panel, Streamlit
- Consistent API: Unified interface regardless of UI framework
- Extensible: Easy to add new frameworks (Gradio, Dash, etc.)
- Type safety:
ChatMessage provides clear message structure
- Testable: Framework-agnostic components are easy to test
Implementation Priority
- Phase 1: Core framework-agnostic components + Marimo implementation
- Phase 2: Panel implementation
- Phase 3: Streamlit implementation
- Phase 4: Additional frameworks (Gradio, Dash, etc.)
Example Usage
# Marimo
def chat_turn(messages, config):
chat = MultiMessageChat(MarimoRenderer(mo))
agent = lmb.AgentBot(on_message_callback=chat.add_message)
result = agent(messages[-1].content)
yield chat.add_text(result.content)
# Panel
chat_interface = PanelChatInterface()
agent = lmb.AgentBot(on_message_callback=chat_interface.add_message)
result = agent(user_input)
# Streamlit
chat_interface = StreamlitChatInterface()
agent = lmb.AgentBot(on_message_callback=chat_interface.add_message)
result = agent(user_input)
This design provides a clean, framework-agnostic way to handle multi-message chat responses across different UI frameworks!
Status
Ready for implementation when needed. All architectural decisions made and implementation plan detailed.
Related
- Current ToolBot pattern in
notebooks/toolbot_chatdata.py
- Marimo chat UI documentation
- AgentBot ReAct loop implementation
Overview
Enable AgentBot/ToolBot to return multiple separate chat messages across different UI frameworks (Marimo, Panel, Streamlit), allowing for rich responses with plots, dataframes, and text as separate messages.
Problem
Currently, chat responses are limited to a single message that can only contain one type of content. Users want to see:
Architecture Decision
Use framework-agnostic design with streaming/callback pattern where AgentBot yields messages during its ReAct loop instead of returning a single response at the end.
Framework-agnostic design: Start with Marimo as reference implementation, then create adapters for Panel and Streamlit.
Implementation Approach
1. Create Framework-Agnostic Message System
Create
llamabot/components/multi_message.py:ChatMessagedataclass for consistent message structureMessageRendererabstract base class for framework-specific renderingMarimoRenderer,PanelRenderer,StreamlitRenderer2. Create Framework-Agnostic Chat Interface
Create
llamabot/components/chat_interface.py:MultiMessageChatclass with unified API across frameworksadd_text(),add_plot(),add_dataframe(),add_code()3. Update AgentBot with Streaming Support
Modify
llamabot/bot/agentbot.py:on_message_callbackparameter to__init__callback(message: ChatMessage) -> None4. Framework-Specific Implementations
Marimo Implementation
Panel Implementation
Streamlit Implementation
Files to Create/Modify
Core Framework-Agnostic Components
llamabot/components/multi_message.py(ChatMessage, MessageRenderer, framework renderers)llamabot/components/chat_interface.py(MultiMessageChat class)tests/components/test_multi_message.pytests/components/test_chat_interface.pyFramework-Specific Implementations
llamabot/components/marimo_chat.py(Marimo-specific chat interface)llamabot/components/panel_chat.py(Panel-specific chat interface)llamabot/components/streamlit_chat.py(Streamlit-specific chat interface)AgentBot Updates
llamabot/bot/agentbot.py(add callback support)tests/bot/test_agentbot_streaming.pyExample Notebooks
notebooks/toolbot_chatdata.py(use new framework-agnostic interface)examples/panel_multi_message_chat.pyexamples/streamlit_multi_message_chat.pyKey Design Decisions
ChatMessageandMessageRendererabstract the differencesMultiMessageChatprovides consistent API across frameworksChatMessageobjectsBenefits
ChatMessageprovides clear message structureImplementation Priority
Example Usage
This design provides a clean, framework-agnostic way to handle multi-message chat responses across different UI frameworks!
Status
Ready for implementation when needed. All architectural decisions made and implementation plan detailed.
Related
notebooks/toolbot_chatdata.py