youngjin-langchain-tools 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: youngjin-langchain-tools
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: LangGraph utilities for Streamlit - StreamlitLanggraphHandler and more
5
5
  Project-URL: Homepage, https://github.com/yourusername/youngjin-langchain-tools
6
6
  Project-URL: Documentation, https://github.com/yourusername/youngjin-langchain-tools#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "youngjin-langchain-tools"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "LangGraph utilities for Streamlit - StreamlitLanggraphHandler and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,8 +8,8 @@ LangGraph agent responses in Streamlit applications.
8
8
  Replaces the deprecated StreamlitCallbackHandler for LangGraph-based agents.
9
9
  """
10
10
 
11
- from typing import Any, Dict, List, Optional, Union, Generator
12
- from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional, Generator
12
+ from dataclasses import dataclass
13
13
  import logging
14
14
  import re
15
15
 
@@ -129,6 +129,13 @@ class StreamlitLanggraphHandlerConfig:
129
129
  expand_new_thoughts: bool = True
130
130
  """Whether to expand the status container to show tool calls."""
131
131
 
132
+ collapse_completed_thoughts: bool = True
133
+ """Whether to automatically collapse completed thought containers."""
134
+
135
+ max_thought_containers: int = 4
136
+ """Maximum number of thought containers to show at once.
137
+ When exceeded, oldest thoughts are moved to a 'History' expander."""
138
+
132
139
  max_tool_content_length: int = 2000
133
140
  """Maximum length of tool output to display before truncating."""
134
141
 
@@ -202,6 +209,8 @@ class StreamlitLanggraphHandler:
202
209
  container: Any,
203
210
  *,
204
211
  expand_new_thoughts: bool = True,
212
+ collapse_completed_thoughts: bool = True,
213
+ max_thought_containers: int = 4,
205
214
  max_tool_content_length: int = 2000,
206
215
  show_tool_calls: bool = True,
207
216
  show_tool_results: bool = True,
@@ -217,6 +226,11 @@ class StreamlitLanggraphHandler:
217
226
  Usually st.container() or similar.
218
227
  expand_new_thoughts: Whether to expand status container
219
228
  to show tool calls. Defaults to True.
229
+ collapse_completed_thoughts: Whether to collapse completed
230
+ thought containers. Defaults to True.
231
+ max_thought_containers: Maximum number of thought containers
232
+ to show at once. Oldest are moved to
233
+ 'History' expander. Defaults to 4.
220
234
  max_tool_content_length: Maximum characters of tool output
221
235
  to display. Defaults to 2000.
222
236
  show_tool_calls: Whether to show tool call info. Defaults to True.
@@ -230,6 +244,8 @@ class StreamlitLanggraphHandler:
230
244
  else:
231
245
  self._config = StreamlitLanggraphHandlerConfig(
232
246
  expand_new_thoughts=expand_new_thoughts,
247
+ collapse_completed_thoughts=collapse_completed_thoughts,
248
+ max_thought_containers=max_thought_containers,
233
249
  max_tool_content_length=max_tool_content_length,
234
250
  show_tool_calls=show_tool_calls,
235
251
  show_tool_results=show_tool_results,
@@ -241,6 +257,9 @@ class StreamlitLanggraphHandler:
241
257
  self._final_response: str = ""
242
258
  self._status_container: Any = None
243
259
  self._response_placeholder: Any = None
260
+ self._thoughts_placeholder: Any = None # Placeholder for thoughts area
261
+ self._thought_history: List[Dict[str, Any]] = [] # History of old thoughts
262
+ self._current_thoughts: List[Dict[str, Any]] = [] # Current visible thoughts
244
263
 
245
264
  @property
246
265
  def config(self) -> StreamlitLanggraphHandlerConfig:
@@ -332,9 +351,13 @@ class StreamlitLanggraphHandler:
332
351
 
333
352
  # Reset state
334
353
  self._final_response = ""
354
+ self._thought_history = []
355
+ self._current_thoughts = []
335
356
 
336
357
  # Create UI components
337
358
  with self._container:
359
+ # Thoughts area placeholder (manages history + current thoughts)
360
+ self._thoughts_placeholder = st.empty()
338
361
  self._status_container = st.status(
339
362
  self._config.thinking_label,
340
363
  expanded=self._config.expand_new_thoughts
@@ -405,16 +428,75 @@ class StreamlitLanggraphHandler:
405
428
 
406
429
  yield {"type": "complete", "data": {"response": self._final_response}}
407
430
 
431
+ def _add_thought(self, thought_type: str, data: Dict[str, Any]) -> None:
432
+ """Add a thought and manage history if max_thought_containers exceeded."""
433
+ thought = {"type": thought_type, "data": data}
434
+ self._current_thoughts.append(thought)
435
+
436
+ # Check if we need to move old thoughts to history
437
+ max_containers = self._config.max_thought_containers
438
+ if len(self._current_thoughts) > max_containers:
439
+ # Move oldest thoughts to history
440
+ while len(self._current_thoughts) > max_containers:
441
+ old_thought = self._current_thoughts.pop(0)
442
+ self._thought_history.append(old_thought)
443
+
444
+ # Re-render the entire thoughts area
445
+ self._render_thoughts()
446
+
447
+ def _render_thought_item(self, thought: Dict[str, Any], in_history: bool = False) -> None:
448
+ """Render a single thought item."""
449
+ import streamlit as st
450
+
451
+ if thought["type"] == "tool_call":
452
+ if self._config.show_tool_calls:
453
+ st.markdown(
454
+ f"{self._config.tool_call_emoji} "
455
+ f"**{thought['data']['name']}**: `{thought['data']['args']}`"
456
+ )
457
+ elif thought["type"] == "tool_result":
458
+ if self._config.show_tool_results:
459
+ tool_name = thought['data']['name']
460
+ content = thought['data']['content']
461
+
462
+ st.markdown(
463
+ f"{self._config.tool_complete_emoji} "
464
+ f"**{tool_name}** 완료"
465
+ )
466
+
467
+ if in_history:
468
+ # Shorter preview in history
469
+ max_len = 200
470
+ else:
471
+ max_len = self._config.max_tool_content_length
472
+
473
+ expanded = not self._config.collapse_completed_thoughts
474
+ with st.expander(f"📋 {tool_name} 결과 보기", expanded=expanded if not in_history else False):
475
+ if len(content) > max_len:
476
+ st.code(content[:max_len] + "\n... (truncated)", language="text")
477
+ else:
478
+ st.code(content, language="text")
479
+
480
+ def _render_thoughts(self) -> None:
481
+ """Render all thoughts (history + current) in the placeholder."""
482
+ import streamlit as st
483
+
484
+ with self._thoughts_placeholder.container():
485
+ # Render history expander if there are old thoughts
486
+ if self._thought_history:
487
+ with st.expander(f"📜 History ({len(self._thought_history)} items)", expanded=False):
488
+ for thought in self._thought_history:
489
+ self._render_thought_item(thought, in_history=True)
490
+
491
+ # Render current thoughts
492
+ for thought in self._current_thoughts:
493
+ self._render_thought_item(thought, in_history=False)
494
+
408
495
  def _handle_updates(
409
496
  self,
410
497
  data: Dict[str, Any]
411
498
  ) -> Generator[Dict[str, Any], None, None]:
412
499
  """Handle 'updates' stream mode events."""
413
- try:
414
- import streamlit as st
415
- except ImportError:
416
- return
417
-
418
500
  for source, update in data.items():
419
501
  if not isinstance(update, dict):
420
502
  continue
@@ -423,48 +505,43 @@ class StreamlitLanggraphHandler:
423
505
  for msg in messages:
424
506
  # Handle tool calls
425
507
  if hasattr(msg, 'tool_calls') and msg.tool_calls:
426
- if self._config.show_tool_calls:
427
- for tc in msg.tool_calls:
428
- tool_name = tc.get('name', 'tool')
429
- tool_args = tc.get('args', {})
430
-
431
- with self._status_container:
432
- st.write(
433
- f"{self._config.tool_call_emoji} "
434
- f"**{tool_name}**: `{tool_args}`"
435
- )
436
-
437
- yield {
438
- "type": "tool_call",
439
- "data": {"name": tool_name, "args": tool_args}
440
- }
508
+ for tc in msg.tool_calls:
509
+ tool_name = tc.get('name', 'tool')
510
+ tool_args = tc.get('args', {})
441
511
 
442
- # Handle tool results
443
- if source == "tools" and hasattr(msg, 'name'):
444
- if self._config.show_tool_results:
445
- tool_name = msg.name
446
- tool_content = str(msg.content) if hasattr(msg, 'content') else ""
447
-
448
- with self._status_container:
449
- st.write(
450
- f"{self._config.tool_complete_emoji} "
451
- f"**{tool_name}** 완료"
452
- )
453
- with st.expander(f"📋 {tool_name} 결과 보기", expanded=False):
454
- if len(tool_content) > self._config.max_tool_content_length:
455
- st.code(
456
- tool_content[:self._config.max_tool_content_length]
457
- + "\n... (truncated)",
458
- language="text"
459
- )
460
- else:
461
- st.code(tool_content, language="text")
512
+ # Add to thought tracking (this triggers re-render)
513
+ self._add_thought("tool_call", {"name": tool_name, "args": tool_args})
514
+
515
+ # Update status label to show current action
516
+ self._status_container.update(
517
+ label=f"{self._config.tool_call_emoji} {tool_name}...",
518
+ state="running"
519
+ )
462
520
 
463
521
  yield {
464
- "type": "tool_result",
465
- "data": {"name": tool_name, "content": tool_content}
522
+ "type": "tool_call",
523
+ "data": {"name": tool_name, "args": tool_args}
466
524
  }
467
525
 
526
+ # Handle tool results
527
+ if source == "tools" and hasattr(msg, 'name'):
528
+ tool_name = msg.name
529
+ tool_content = str(msg.content) if hasattr(msg, 'content') else ""
530
+
531
+ # Add to thought tracking (this triggers re-render)
532
+ self._add_thought("tool_result", {"name": tool_name, "content": tool_content})
533
+
534
+ # Update status to show thinking again
535
+ self._status_container.update(
536
+ label=self._config.thinking_label,
537
+ state="running"
538
+ )
539
+
540
+ yield {
541
+ "type": "tool_result",
542
+ "data": {"name": tool_name, "content": tool_content}
543
+ }
544
+
468
545
  def _handle_messages(
469
546
  self,
470
547
  data: tuple