Skip to content

Commit 1ad3691

Browse files
committed
Add detailed comments to improve code readability
1 parent 94c77d1 commit 1ad3691

File tree

3 files changed

+65
-12
lines changed

3 files changed

+65
-12
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ A reference implementation for building chat applications with LangGraph and Str
1515
- **Multimodal Support**: Image input handling for compatible models
1616
- **Extensible Agent System**: Abstract base class (`AgentConfig`) makes it straightforward to add custom agents with their own configuration UI
1717

18-
## Supported Providers
18+
## Implemented Providers
1919

2020
- **OpenAI**: GPT-4o, GPT-4 Turbo, o1, o3-mini
2121
- **Anthropic**: Claude Sonnet 4, Claude 3.7 Sonnet, Claude 3.5 Sonnet/Haiku
2222

23+
Additional providers can be easily added by implementing the `AgentConfig` interface (see [Adding Custom Agents](#adding-custom-agents)).
24+
2325
## Quick Start
2426

2527
1. **Clone and install:**

app/app.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,24 @@
2020

2121

2222
def main():
23+
"""Main Streamlit application for LangGraph Agent Chat."""
2324
st.title("LangGraph Agent Chat")
2425
initialize_checkpoint()
2526

27+
# === Sidebar: Conversation and Agent Management ===
2628
with st.sidebar:
2729
st.header("💬 Conversations")
2830

29-
# Thread management
31+
# Thread management: Get existing threads and determine current thread
3032
threads, latest = get_threads(st.session_state.checkpoint)
3133
current = st.session_state.get('thread_id') or latest or str(uuid.uuid4())
3234
st.session_state.thread_id = current
3335

36+
# Ensure current thread appears in the list
3437
if current not in threads:
3538
threads.insert(0, current)
3639

40+
# Thread selection dropdown
3741
st.selectbox(
3842
"Select thread",
3943
threads,
@@ -45,6 +49,7 @@ def main():
4549
st.button("New chat", on_click=lambda: setattr(st.session_state, 'thread_id', str(uuid.uuid4())))
4650
st.button("Delete current chat", on_click=on_delete_thread, args=(st.session_state.thread_id,))
4751

52+
# Streaming toggle
4853
use_streaming = st.checkbox(
4954
"Enable Streaming",
5055
value=True,
@@ -55,7 +60,7 @@ def main():
5560
st.divider()
5661
st.header("⚙️ Agent Settings")
5762

58-
# Agent selection
63+
# Agent selection: Display available agents
5964
agent_names = [agent.get_name() for agent in AVAILABLE_AGENTS]
6065
if not agent_names:
6166
st.warning("No agents configured. Please add agents to AVAILABLE_AGENTS.")
@@ -73,10 +78,11 @@ def main():
7378
st.warning("No agents configured. Please add agents to AVAILABLE_AGENTS.")
7479
st.stop()
7580

81+
# Render agent-specific configuration UI
7682
st.subheader("Agent Options")
7783
options = selected_agent_config.render_options()
7884

79-
# Rebuild agent if needed
85+
# Rebuild agent if configuration changed
8086
prev_name = st.session_state.get("current_agent_name")
8187
needs_rebuild = (
8288
prev_name != selected_name or
@@ -90,65 +96,75 @@ def main():
9096
st.session_state.current_agent_name = selected_name
9197
st.session_state.agent_options = options
9298

99+
# === Main Area: Chat Interface ===
100+
# Chat input with multimodal support (text + images)
93101
submission = st.chat_input(
94102
"Ask me anything!",
95103
accept_file=True,
96104
file_type=["jpg", "jpeg", "png"],
97105
)
98106

107+
# Display conversation history
99108
display_chat_history(st.session_state.checkpoint, st.session_state.thread_id)
100109

110+
# Handle new user input
101111
if submission:
102-
# Parse submission
112+
# Parse submission (handles both dict and object formats)
103113
if isinstance(submission, dict):
104114
user_text = submission.get("text", "")
105115
user_files = submission.get("files", [])
106116
else:
107117
user_text = getattr(submission, "text", submission if isinstance(submission, str) else "")
108118
user_files = getattr(submission, "files", [])
109119

120+
# Convert to LangChain message format
110121
content = convert_input_to_content(user_text, user_files)
111122

112123
# Display user message
113124
with st.chat_message("user"):
114125
render_content(content)
115126

116-
# Assistant response
127+
# Generate and display assistant response
117128
use_streaming = st.session_state.get("use_streaming", True)
118129

119130
with st.chat_message("assistant"):
120131
if use_streaming:
121-
# Stream response with fixed order: thinking → tools → text
132+
# Streaming mode: Display thinking, tools, and text in separate containers
133+
# This ensures proper ordering even when messages arrive out of order
122134
thinking_container = st.container()
123135
tools_container = st.container()
124136
text_container = st.container()
125137
text_buffer = []
126138

127139
def render_to_container(title, payload):
140+
"""Route thinking/tool messages to appropriate containers."""
128141
target = thinking_container if "Thinking" in title else tools_container
129142
with target:
130143
render_tool(title, payload)
131144

132145
with text_container:
133146
text_element = st.empty()
134147

148+
# Stream agent response
135149
stream = st.session_state.agent.stream(
136150
{"messages": [HumanMessage(content=content)]}, # type: ignore[arg-type]
137151
config={"configurable": {"thread_id": st.session_state.thread_id}},
138152
stream_mode="messages",
139153
)
140154

155+
# Process stream and display text chunks incrementally
141156
for text_chunk in extract_text_chunks(stream, tool_callback=render_to_container):
142157
text_buffer.append(text_chunk)
143158
text_element.markdown("".join(text_buffer))
144159
else:
145-
# Invoke (non-streaming)
160+
# Non-streaming mode: Invoke agent and wait for complete response
146161
with st.spinner("Processing..."):
147162
st.session_state.agent.invoke(
148163
{"messages": [HumanMessage(content=content)]}, # type: ignore[arg-type]
149164
config={"configurable": {"thread_id": st.session_state.thread_id}},
150165
)
151166

167+
# Rerun to display the new message from history
152168
st.rerun()
153169

154170

app/utils.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,31 @@ def extract_text_chunks(
2020
message_stream: Iterator[Any],
2121
tool_callback: Callable[[str, Any], None] | None = None
2222
) -> Iterator[str]:
23-
"""Extract text chunks from a stream, handling tool and thinking messages separately."""
23+
"""Extract text chunks from a stream, handling tool and thinking messages separately.
24+
25+
This function processes streaming responses from LLM agents and separates:
26+
- Text content: Yielded for display
27+
- Thinking blocks: Buffered and sent via callback
28+
- Tool calls: Sent via callback immediately
29+
30+
Args:
31+
message_stream: Stream of message events from the agent
32+
tool_callback: Optional callback for tool/thinking messages
33+
34+
Yields:
35+
Text chunks to be displayed to the user
36+
"""
37+
# Buffer for accumulating thinking content before displaying
2438
thinking_buffer: list[str] = []
2539

2640
def flush_thinking() -> None:
41+
"""Send buffered thinking content via callback and clear buffer."""
2742
if thinking_buffer and tool_callback:
2843
tool_callback("💭 Thinking", "".join(thinking_buffer))
2944
thinking_buffer.clear()
3045

3146
for event in message_stream:
47+
# Unwrap event tuple if needed
3248
chunk = event[0] if isinstance(event, tuple) else event
3349
msg_type = getattr(chunk, "type", None)
3450

@@ -42,7 +58,7 @@ def flush_thinking() -> None:
4258

4359
content = getattr(chunk, "content", None)
4460

45-
# Anthropic: content is list of parts
61+
# Anthropic: content is a list of parts (multimodal/thinking/text)
4662
if isinstance(content, list):
4763
for part in content:
4864
if not isinstance(part, dict):
@@ -51,19 +67,23 @@ def flush_thinking() -> None:
5167
part_type = part.get("type")
5268

5369
if part_type == "thinking":
70+
# Accumulate thinking content in buffer
5471
thinking_buffer.append(part.get("thinking", ""))
5572
elif part_type == "tool_use":
73+
# Flush thinking before tool use
5674
flush_thinking()
5775
elif part_type == "text":
76+
# Flush thinking before displaying text
5877
flush_thinking()
5978
if text := part.get("text"):
6079
yield text
6180

62-
# OpenAI: content is string
81+
# OpenAI: content is a simple string
6382
elif isinstance(content, str):
6483
flush_thinking()
6584
yield content
6685

86+
# Flush any remaining thinking content at the end
6787
flush_thinking()
6888

6989

@@ -143,14 +163,29 @@ def render_content(content: str | list[Any]) -> None:
143163

144164

145165
def convert_input_to_content(user_text: str, user_files: list[Any]) -> str | list[dict[str, Any]]:
146-
"""Convert Streamlit chat input to LangChain message content format."""
166+
"""Convert Streamlit chat input to LangChain message content format.
167+
168+
Handles both text-only and multimodal (text + images) inputs.
169+
- Text only: Returns simple string
170+
- Text + images: Returns list of content parts with base64-encoded images
171+
172+
Args:
173+
user_text: User's text input
174+
user_files: List of uploaded files from Streamlit
175+
176+
Returns:
177+
String for text-only, or list of content parts for multimodal input
178+
"""
179+
# Text-only input: return as simple string
147180
if not user_files:
148181
return user_text.strip() if isinstance(user_text, str) else ""
149182

183+
# Multimodal input: build list of content parts
150184
parts: list[dict[str, Any]] = []
151185
if isinstance(user_text, str) and user_text.strip():
152186
parts.append({"type": "text", "text": user_text.strip()})
153187

188+
# Convert uploaded images to base64 data URLs
154189
for f in user_files:
155190
mime_type = getattr(f, 'type', None) or 'image/png'
156191
encoded = base64.b64encode(f.getvalue()).decode('utf-8')

0 commit comments

Comments
 (0)