langfun 0.1.2.dev202410080804__py3-none-any.whl → 0.1.2.dev202410110804__py3-none-any.whl

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.
langfun/core/logging.py CHANGED
@@ -13,16 +13,13 @@
13
13
  # limitations under the License.
14
14
  """Langfun event logging."""
15
15
 
16
- from collections.abc import Iterator
17
16
  import contextlib
18
17
  import datetime
19
- import io
20
18
  import typing
21
- from typing import Any, Literal
19
+ from typing import Any, Iterator, Literal, Sequence
22
20
 
23
21
  from langfun.core import component
24
22
  from langfun.core import console
25
- from langfun.core import repr_utils
26
23
  import pyglove as pg
27
24
 
28
25
 
@@ -56,49 +53,153 @@ class LogEntry(pg.Object):
56
53
  def should_output(self, min_log_level: LogLevel) -> bool:
57
54
  return _LOG_LEVELS.index(self.level) >= _LOG_LEVELS.index(min_log_level)
58
55
 
59
- def _repr_html_(self) -> str:
60
- s = io.StringIO()
61
- padding_left = 50 * self.indent
62
- s.write(f'<div style="padding-left: {padding_left}px;">')
63
- s.write(self._message_display)
64
- if self.metadata:
65
- s.write(repr_utils.html_repr(self.metadata))
66
- s.write('</div>')
67
- return s.getvalue()
68
-
69
- @property
70
- def _message_text_bgcolor(self) -> str:
71
- match self.level:
72
- case 'debug':
73
- return '#EEEEEE'
74
- case 'info':
75
- return '#A3E4D7'
76
- case 'warning':
77
- return '#F8C471'
78
- case 'error':
79
- return '#F5C6CB'
80
- case 'fatal':
81
- return '#F19CBB'
82
- case _:
83
- raise ValueError(f'Unknown log level: {self.level}')
84
-
85
- @property
86
- def _time_display(self) -> str:
87
- display_text = self.time.strftime('%H:%M:%S')
88
- alt_text = self.time.strftime('%Y-%m-%d %H:%M:%S.%f')
89
- return (
90
- '<span style="background-color: #BBBBBB; color: white; '
91
- 'border-radius:5px; padding:0px 5px 0px 5px;" '
92
- f'title="{alt_text}">{display_text}</span>'
56
+ def _html_tree_view_summary(
57
+ self,
58
+ view: pg.views.HtmlTreeView,
59
+ title: str | pg.Html | None = None,
60
+ max_str_len_for_summary: int = pg.View.PresetArgValue(80), # pytype: disable=annotation-type-mismatch
61
+ **kwargs
62
+ ) -> str:
63
+ if len(self.message) > max_str_len_for_summary:
64
+ message = self.message[:max_str_len_for_summary] + '...'
65
+ else:
66
+ message = self.message
67
+
68
+ s = pg.Html(
69
+ pg.Html.element(
70
+ 'span',
71
+ [self.time.strftime('%H:%M:%S')],
72
+ css_class=['log-time']
73
+ ),
74
+ pg.Html.element(
75
+ 'span',
76
+ [pg.Html.escape(message)],
77
+ css_class=['log-summary'],
78
+ ),
79
+ )
80
+ return view.summary(
81
+ self,
82
+ title=title or s,
83
+ max_str_len_for_summary=max_str_len_for_summary,
84
+ **kwargs,
93
85
  )
94
86
 
95
- @property
96
- def _message_display(self) -> str:
97
- return repr_utils.html_round_text(
98
- self._time_display + '&nbsp;' + self.message,
99
- background_color=self._message_text_bgcolor,
87
+ # pytype: disable=annotation-type-mismatch
88
+ def _html_tree_view_content(
89
+ self,
90
+ view: pg.views.HtmlTreeView,
91
+ root_path: pg.KeyPath,
92
+ collapse_log_metadata_level: int = pg.View.PresetArgValue(0),
93
+ max_str_len_for_summary: int = pg.View.PresetArgValue(80),
94
+ **kwargs
95
+ ) -> pg.Html:
96
+ # pytype: enable=annotation-type-mismatch
97
+ def render_message_text():
98
+ if len(self.message) < max_str_len_for_summary:
99
+ return None
100
+ return pg.Html.element(
101
+ 'span',
102
+ [pg.Html.escape(self.message)],
103
+ css_class=['log-text'],
104
+ )
105
+
106
+ def render_metadata():
107
+ if not self.metadata:
108
+ return None
109
+ return pg.Html.element(
110
+ 'div',
111
+ [
112
+ view.render(
113
+ self.metadata,
114
+ name='metadata',
115
+ root_path=root_path + 'metadata',
116
+ parent=self,
117
+ collapse_level=(
118
+ root_path.depth + collapse_log_metadata_level + 1
119
+ )
120
+ )
121
+ ],
122
+ css_class=['log-metadata'],
123
+ )
124
+
125
+ return pg.Html.element(
126
+ 'div',
127
+ [
128
+ render_message_text(),
129
+ render_metadata(),
130
+ ],
131
+ css_class=['complex_value'],
100
132
  )
101
133
 
134
+ def _html_style(self) -> list[str]:
135
+ return super()._html_style() + [
136
+ """
137
+ .log-time {
138
+ color: #222;
139
+ font-size: 12px;
140
+ padding-right: 10px;
141
+ }
142
+ .log-summary {
143
+ font-weight: normal;
144
+ font-style: italic;
145
+ padding: 4px;
146
+ }
147
+ .log-debug > summary > .summary_title::before {
148
+ content: '🛠️ '
149
+ }
150
+ .log-info > summary > .summary_title::before {
151
+ content: '💡 '
152
+ }
153
+ .log-warning > summary > .summary_title::before {
154
+ content: '❗ '
155
+ }
156
+ .log-error > summary > .summary_title::before {
157
+ content: '❌ '
158
+ }
159
+ .log-fatal > summary > .summary_title::before {
160
+ content: '💀 '
161
+ }
162
+ .log-text {
163
+ display: block;
164
+ color: black;
165
+ font-style: italic;
166
+ padding: 20px;
167
+ border-radius: 5px;
168
+ background: rgba(255, 255, 255, 0.5);
169
+ white-space: pre-wrap;
170
+ }
171
+ details.log-entry {
172
+ margin: 0px 0px 10px;
173
+ border: 0px;
174
+ }
175
+ div.log-metadata {
176
+ margin: 10px 0px 0px 0px;
177
+ }
178
+ .log-metadata > details {
179
+ background-color: rgba(255, 255, 255, 0.5);
180
+ border: 1px solid transparent;
181
+ }
182
+ .log-debug {
183
+ background-color: #EEEEEE
184
+ }
185
+ .log-warning {
186
+ background-color: #F8C471
187
+ }
188
+ .log-info {
189
+ background-color: #A3E4D7
190
+ }
191
+ .log-error {
192
+ background-color: #F5C6CB
193
+ }
194
+ .log-fatal {
195
+ background-color: #F19CBB
196
+ }
197
+ """
198
+ ]
199
+
200
+ def _html_element_class(self) -> Sequence[str] | None:
201
+ return super()._html_element_class() + [f'log-{self.level}']
202
+
102
203
 
103
204
  def log(level: LogLevel,
104
205
  message: str,
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
  """Tests for langfun.core.logging."""
15
15
 
16
+ import datetime
17
+ import inspect
16
18
  import unittest
17
19
 
18
20
  from langfun.core import logging
@@ -52,6 +54,37 @@ class LoggingTest(unittest.TestCase):
52
54
  assert_color(logging.error('hi', indent=2, x=1, y=2), '#F5C6CB')
53
55
  assert_color(logging.fatal('hi', indent=2, x=1, y=2), '#F19CBB')
54
56
 
57
+ def assert_html_content(self, html, expected):
58
+ expected = inspect.cleandoc(expected).strip()
59
+ actual = html.content.strip()
60
+ if actual != expected:
61
+ print(actual)
62
+ self.assertEqual(actual, expected)
63
+
64
+ def test_html(self):
65
+ time = datetime.datetime(2024, 10, 10, 12, 30, 45)
66
+ self.assert_html_content(
67
+ logging.LogEntry(
68
+ level='info', message='5 + 2 > 3',
69
+ time=time, metadata={}
70
+ ).to_html(enable_summary_tooltip=False),
71
+ """
72
+ <details open class="pyglove log-entry log-info"><summary><div class="summary_title"><span class="log-time">12:30:45</span><span class="log-summary">5 + 2 &gt; 3</span></div></summary><div class="complex_value"></div></details>
73
+ """
74
+ )
75
+ self.assert_html_content(
76
+ logging.LogEntry(
77
+ level='error', message='This is a longer message: 5 + 2 > 3',
78
+ time=time, metadata=dict(x=1, y=2)
79
+ ).to_html(
80
+ max_str_len_for_summary=10,
81
+ enable_summary_tooltip=False,
82
+ collapse_log_metadata_level=1
83
+ ),
84
+ """
85
+ <details open class="pyglove log-entry log-error"><summary><div class="summary_title"><span class="log-time">12:30:45</span><span class="log-summary">This is a ...</span></div></summary><div class="complex_value"><span class="log-text">This is a longer message: 5 + 2 &gt; 3</span><div class="log-metadata"><details open class="pyglove dict"><summary><div class="summary_name">metadata</div><div class="summary_title">Dict(...)</div></summary><div class="complex_value dict"><table><tr><td><span class="object_key str">x</span><span class="tooltip key-path">metadata.x</span></td><td><div><span class="simple_value int">1</span></div></td></tr><tr><td><span class="object_key str">y</span><span class="tooltip key-path">metadata.y</span></td><td><div><span class="simple_value int">2</span></div></td></tr></table></div></details></div></div></details>
86
+ """
87
+ )
55
88
 
56
89
  if __name__ == '__main__':
57
90
  unittest.main()
langfun/core/message.py CHANGED
@@ -14,13 +14,11 @@
14
14
  """Messages that are exchanged between users and agents."""
15
15
 
16
16
  import contextlib
17
- import html
18
17
  import io
19
- from typing import Annotated, Any, Optional, Union
18
+ from typing import Annotated, Any, Optional, Sequence, Union
20
19
 
21
20
  from langfun.core import modality
22
21
  from langfun.core import natural_language
23
- from langfun.core import repr_utils
24
22
  import pyglove as pg
25
23
 
26
24
 
@@ -406,6 +404,11 @@ class Message(natural_language.NaturalLanguageFormattable, pg.Object):
406
404
  with pg.notify_on_change(False):
407
405
  self.tags.append(tag)
408
406
 
407
+ def has_tag(self, tag: str | tuple[str, ...]) -> bool:
408
+ if isinstance(tag, str):
409
+ return tag in self.tags
410
+ return any(t in self.tags for t in tag)
411
+
409
412
  #
410
413
  # Message source chain.
411
414
  #
@@ -503,79 +506,244 @@ class Message(natural_language.NaturalLanguageFormattable, pg.Object):
503
506
  v = self.metadata[key]
504
507
  return v.value if isinstance(v, pg.Ref) else v
505
508
 
506
- def _repr_html_(self):
507
- return self.to_html().content
508
-
509
- def to_html(
509
+ # pytype: disable=annotation-type-mismatch
510
+ def _html_tree_view_content(
510
511
  self,
511
- include_message_type: bool = True
512
- ) -> repr_utils.Html:
513
- """Returns the HTML representation of the message."""
514
- s = io.StringIO()
515
- s.write('<div style="padding:0px 10px 0px 10px;">')
516
- # Title bar.
517
- if include_message_type:
518
- s.write(
519
- repr_utils.html_round_text(
520
- self.__class__.__name__,
521
- text_color='white',
522
- background_color=self._text_color(),
512
+ *,
513
+ view: pg.views.HtmlTreeView,
514
+ root_path: pg.KeyPath,
515
+ source_tag: str | Sequence[str] | None = pg.View.PresetArgValue(
516
+ ('lm-input', 'lm-output')
517
+ ),
518
+ include_message_metadata: bool = pg.View.PresetArgValue(True),
519
+ collapse_modalities_in_text: bool = pg.View.PresetArgValue(True),
520
+ collapse_llm_usage: bool = pg.View.PresetArgValue(False),
521
+ collapse_message_result_level: int = pg.View.PresetArgValue(1),
522
+ collapse_message_metadata_level: int = pg.View.PresetArgValue(0),
523
+ collapse_source_message_level: int = pg.View.PresetArgValue(1),
524
+ **kwargs,
525
+ ) -> pg.Html:
526
+ # pytype: enable=annotation-type-mismatch
527
+ """Returns the HTML representation of the message.
528
+
529
+ Args:
530
+ view: The HTML tree view.
531
+ root_path: The root path of the message.
532
+ source_tag: tags to filter source messages. If None, the entire
533
+ source chain will be included.
534
+ include_message_metadata: Whether to include the metadata of the message.
535
+ collapse_modalities_in_text: Whether to collapse the modalities in the
536
+ message text.
537
+ collapse_llm_usage: Whether to collapse the usage in the message.
538
+ collapse_message_result_level: The level to collapse the result in the
539
+ message.
540
+ collapse_message_metadata_level: The level to collapse the metadata in the
541
+ message.
542
+ collapse_source_message_level: The level to collapse the source in the
543
+ message.
544
+ **kwargs: Other keyword arguments.
545
+
546
+ Returns:
547
+ The HTML representation of the message content.
548
+ """
549
+ def render_tags():
550
+ return pg.Html.element(
551
+ 'div',
552
+ [pg.Html.element('span', [tag]) for tag in self.tags],
553
+ css_class=['message-tags'],
554
+ )
555
+
556
+ def render_message_text():
557
+ maybe_reformatted = self.get('formatted_text')
558
+ referred_chunks = {}
559
+ s = pg.Html('<div class="message-text">')
560
+ for chunk in self.chunk(maybe_reformatted):
561
+ if isinstance(chunk, str):
562
+ s.write(s.escape(chunk))
563
+ else:
564
+ assert isinstance(chunk, modality.Modality), chunk
565
+ child_path = root_path + 'metadata' + chunk.referred_name
566
+ s.write(
567
+ pg.Html.element(
568
+ 'div',
569
+ [
570
+ view.render(
571
+ chunk,
572
+ name=chunk.referred_name,
573
+ root_path=child_path,
574
+ collapse_level=child_path.depth + (
575
+ 0 if collapse_modalities_in_text else 1
576
+ )
577
+ )
578
+ ],
579
+ css_class=['modality-in-text'],
580
+ )
523
581
  )
582
+ referred_chunks[chunk.referred_name] = chunk
583
+ s.write('</div>')
584
+ return s
585
+
586
+ def render_result():
587
+ if 'result' not in self.metadata:
588
+ return None
589
+ child_path = root_path + 'metadata' + 'result'
590
+ return pg.Html.element(
591
+ 'div',
592
+ [
593
+ view.render(
594
+ self.result,
595
+ name='result',
596
+ root_path=child_path,
597
+ collapse_level=(
598
+ child_path.depth + collapse_message_result_level
599
+ )
600
+ )
601
+ ],
602
+ css_class=['message-result'],
603
+ )
604
+
605
+ def render_usage():
606
+ if 'usage' not in self.metadata:
607
+ return None
608
+ child_path = root_path + 'metadata' + 'usage'
609
+ return pg.Html.element(
610
+ 'div',
611
+ [
612
+ view.render(
613
+ self.usage,
614
+ name='llm usage',
615
+ root_path=child_path,
616
+ collapse_level=child_path.depth + (
617
+ 0 if collapse_llm_usage else 1
618
+ )
619
+ )
620
+ ],
621
+ css_class=['message-usage'],
622
+ )
623
+
624
+ def render_source_message():
625
+ source = self.source
626
+ while (source is not None
627
+ and source_tag is not None
628
+ and not source.has_tag(source_tag)):
629
+ source = source.source
630
+ if source is not None:
631
+ return view.render(
632
+ self.source,
633
+ name='source',
634
+ root_path=root_path + 'source',
635
+ include_metadata=include_message_metadata,
636
+ collapse_level=(
637
+ root_path.depth + 1 + collapse_source_message_level
638
+ ),
639
+ collapse_source_level=max(0, collapse_source_message_level - 1),
640
+ collapse_modalities=collapse_modalities_in_text,
641
+ collapse_usage=collapse_llm_usage,
642
+ collapse_metadata_level=collapse_message_metadata_level,
643
+ collapse_result_level=collapse_message_result_level,
644
+ )
645
+ return None
646
+
647
+ def render_metadata():
648
+ if not include_message_metadata:
649
+ return None
650
+ child_path = root_path + 'metadata'
651
+ return pg.Html.element(
652
+ 'div',
653
+ [
654
+ view.render(
655
+ self.metadata,
656
+ css_class=['message-metadata'],
657
+ name='metadata',
658
+ root_path=child_path,
659
+ collapse_level=(
660
+ child_path.depth + collapse_message_metadata_level
661
+ )
662
+ )
663
+ ],
664
+ css_class=['message-metadata'],
524
665
  )
525
- s.write('<hr>')
526
666
 
527
- # Body.
528
- s.write(
529
- f'<span style="color: {self._text_color()}; white-space: pre-wrap;">'
667
+ return pg.Html.element(
668
+ 'div',
669
+ [
670
+ render_tags(),
671
+ render_message_text(),
672
+ render_result(),
673
+ render_usage(),
674
+ render_metadata(),
675
+ render_source_message(),
676
+ ],
677
+ css_class=['complex_value'],
530
678
  )
531
679
 
532
- # NOTE(daiyip): LLM may reformat the text from the input, therefore
533
- # we proritize the formatted text if it's available.
534
- maybe_reformatted = self.get('formatted_text')
535
- referred_chunks = {}
536
- for chunk in self.chunk(maybe_reformatted):
537
- if isinstance(chunk, str):
538
- s.write(html.escape(chunk))
539
- else:
540
- assert isinstance(chunk, modality.Modality), chunk
541
- s.write('&nbsp;')
542
- s.write(repr_utils.html_round_text(
543
- chunk.referred_name,
544
- text_color='black',
545
- background_color='#f7dc6f'
546
- ))
547
- s.write('&nbsp;')
548
- referred_chunks[chunk.referred_name] = chunk
549
- s.write('</span>')
550
-
551
- def item_color(k, v):
552
- if isinstance(v, modality.Modality):
553
- return ('black', '#f7dc6f', None, None) # Light yellow
554
- elif k == 'result':
555
- return ('white', 'purple', 'purple', None) # Blue.
556
- elif k in ('usage',):
557
- return ('white', '#e74c3c', None, None) # Red.
558
- else:
559
- return ('white', '#17202a', None, None) # Dark gray
560
-
561
- # TODO(daiyip): Revisit the logic in deciding what metadata keys to
562
- # expose to the user.
563
- if referred_chunks:
564
- s.write(repr_utils.html_repr(referred_chunks, item_color))
565
-
566
- if 'lm-response' in self.tags:
567
- s.write(repr_utils.html_repr(self.metadata, item_color))
568
- s.write('</div>')
569
- return repr_utils.Html(s.getvalue())
570
-
571
- def _text_color(self) -> str:
572
- match self.__class__.__name__:
573
- case 'UserMessage':
574
- return 'green'
575
- case 'AIMessage':
576
- return 'blue'
577
- case _:
578
- return 'black'
680
+ def _html_style(self) -> list[str]:
681
+ return super()._html_style() + [
682
+ """
683
+ /* Langfun Message styles.*/
684
+ [class^="message-"] > details {
685
+ margin: 0px 0px 5px 0px;
686
+ border: 1px solid #EEE;
687
+ }
688
+ details.lf-message > summary > .summary_title::after {
689
+ content: ' 💬';
690
+ }
691
+ details.pyglove.ai-message {
692
+ border: 1px solid blue;
693
+ color: blue;
694
+ }
695
+ details.pyglove.user-message {
696
+ border: 1px solid green;
697
+ color: green;
698
+ }
699
+ .message-tags {
700
+ margin: 5px 0px 5px 0px;
701
+ font-size: .8em;
702
+ }
703
+ .message-tags > span {
704
+ border-radius: 5px;
705
+ background-color: #CCC;
706
+ padding: 3px;
707
+ margin: 0px 2px 0px 2px;
708
+ color: white;
709
+ }
710
+ .message-text {
711
+ padding: 20px;
712
+ margin: 10px 5px 10px 5px;
713
+ font-style: italic;
714
+ font-size: 1.1em;
715
+ white-space: pre-wrap;
716
+ border: 1px solid #EEE;
717
+ border-radius: 5px;
718
+ background-color: #EEE;
719
+ }
720
+ .modality-in-text {
721
+ display: inline-block;
722
+ }
723
+ .modality-in-text > details {
724
+ display: inline-block;
725
+ font-size: 0.8em;
726
+ border: 0;
727
+ background-color: #A6F1A6;
728
+ margin: 0px 5px 0px 5px;
729
+ }
730
+ .message-result {
731
+ color: purple;
732
+ }
733
+ .message-usage {
734
+ color: orange;
735
+ }
736
+ .message-usage .object_key.str {
737
+ border: 1px solid orange;
738
+ background-color: orange;
739
+ color: white;
740
+ }
741
+ """
742
+ ]
743
+
744
+ def _html_element_class(self) -> list[str]:
745
+ return super()._html_element_class() + ['lf-message']
746
+
579
747
 
580
748
  #
581
749
  # Messages of different roles.