diff --git a/langfun/core/component.py b/langfun/core/component.py index a8838e7..006f727 100644 --- a/langfun/core/component.py +++ b/langfun/core/component.py @@ -286,6 +286,54 @@ def value_from( else: return pg.MISSING_VALUE + def _html_tree_view_content( + self, + *, + view: pg.views.HtmlTreeView, + parent: Any, + root_path: pg.KeyPath, + **kwargs, + ) -> pg.Html: + inferred_value = pg.MISSING_VALUE + if isinstance(parent, pg.Symbolic) and root_path: + inferred_value = parent.sym_inferred(root_path.key, pg.MISSING_VALUE) + + if inferred_value is not pg.MISSING_VALUE: + kwargs.pop('name', None) + return view.render( + inferred_value, parent=self, root_path=root_path + '', + **view.get_passthrough_kwargs(**kwargs) + ) + return pg.Html.element( + 'div', + [ + '(not available)', + ], + css_classes=['unavailable-contextual'], + ) + + def _html_tree_view_config(self) -> dict[str, Any]: + return pg.views.HtmlTreeView.get_kwargs( + super()._html_tree_view_config(), + dict( + collapse_level=1, + ) + ) + + @classmethod + def _html_tree_view_css_styles(cls) -> list[str]: + return super()._html_tree_view_css_styles() + [ + """ + .contextual-attribute { + color: purple; + } + .unavailable-contextual { + color: gray; + font-style: italic; + } + """ + ] + # NOTE(daiyip): Returning Any instead of `lf.ContextualAttribute` to avoid # pytype check error as `contextual()` can be assigned to any type. diff --git a/langfun/core/component_test.py b/langfun/core/component_test.py index d78a614..682a3b2 100644 --- a/langfun/core/component_test.py +++ b/langfun/core/component_test.py @@ -13,6 +13,8 @@ # limitations under the License. """Contextual component and app test.""" +import inspect +from typing import Any import unittest import weakref @@ -297,6 +299,52 @@ class C(lf.Component): self.assertEqual(c.z, 3) self.assertEqual(b.z, 3) + def test_to_html(self): + class A(lf.Component): + x: int = 1 + y: int = lf.contextual() + + def assert_content(html, expected): + expected = inspect.cleandoc(expected).strip() + actual = html.content.strip() + if actual != expected: + print(actual) + self.assertEqual(actual.strip(), expected) + + self.assertIn( + inspect.cleandoc( + """ + .contextual-attribute { + color: purple; + } + .unavailable-contextual { + color: gray; + font-style: italic; + } + """ + ), + A().to_html().style_section, + ) + + assert_content( + A().to_html(enable_summary_tooltip=False), + """ +
A(...)
xx
int
1
yy
ContextualAttribute(...)
(not available)
+ """ + ) + + class B(lf.Component): + z: Any + y: int = 2 + + b = B(A()) + assert_content( + b.z.to_html(enable_summary_tooltip=False), + """ +
A(...)
xx
int
1
yy
ContextualAttribute(...)
2
+ """ + ) + if __name__ == '__main__': unittest.main() diff --git a/langfun/core/logging.py b/langfun/core/logging.py index 941d87d..7b87e7c 100644 --- a/langfun/core/logging.py +++ b/langfun/core/logging.py @@ -15,8 +15,9 @@ import contextlib import datetime +import functools import typing -from typing import Any, Iterator, Literal, Sequence +from typing import Any, Iterator, Literal from langfun.core import component from langfun.core import console @@ -57,11 +58,11 @@ def _html_tree_view_summary( self, view: pg.views.HtmlTreeView, title: str | pg.Html | None = None, - max_str_len_for_summary: int = pg.View.PresetArgValue(80), # pytype: disable=annotation-type-mismatch + max_summary_len_for_str: int = 80, **kwargs ) -> str: - if len(self.message) > max_str_len_for_summary: - message = self.message[:max_str_len_for_summary] + '...' + if len(self.message) > max_summary_len_for_str: + message = self.message[:max_summary_len_for_str] + '...' else: message = self.message @@ -69,18 +70,18 @@ def _html_tree_view_summary( pg.Html.element( 'span', [self.time.strftime('%H:%M:%S')], - css_class=['log-time'] + css_classes=['log-time'] ), pg.Html.element( 'span', [pg.Html.escape(message)], - css_class=['log-summary'], + css_classes=['log-summary'], ), ) return view.summary( self, title=title or s, - max_str_len_for_summary=max_str_len_for_summary, + max_summary_len_for_str=max_summary_len_for_str, **kwargs, ) @@ -89,43 +90,45 @@ def _html_tree_view_content( self, view: pg.views.HtmlTreeView, root_path: pg.KeyPath, - collapse_log_metadata_level: int | None = pg.View.PresetArgValue(0), - max_str_len_for_summary: int = pg.View.PresetArgValue(80), - collapse_level: int | None = pg.View.PresetArgValue(1), + max_summary_len_for_str: int = 80, + collapse_level: int | None = 1, + extra_flags: dict[str, Any] | None = None, **kwargs ) -> pg.Html: # pytype: enable=annotation-type-mismatch + extra_flags = extra_flags if extra_flags is not None else {} + collapse_log_metadata_level: int | None = extra_flags.get( + 'collapse_log_metadata_level', None + ) def render_message_text(): - if len(self.message) < max_str_len_for_summary: + if len(self.message) < max_summary_len_for_str: return None return pg.Html.element( 'span', [pg.Html.escape(self.message)], - css_class=['log-text'], + css_classes=['log-text'], ) def render_metadata(): if not self.metadata: return None - child_path = root_path + 'metadata' return pg.Html.element( 'div', [ view.render( self.metadata, name='metadata', - root_path=child_path, + root_path=root_path + 'metadata', parent=self, - collapse_level=( - view.max_collapse_level( - collapse_level, - collapse_log_metadata_level, - child_path - ) - ) + collapse_level=view.get_collapse_level( + (collapse_level, -1), collapse_log_metadata_level, + ), + max_summary_len_for_str=max_summary_len_for_str, + extra_flags=extra_flags, + **view.get_passthrough_kwargs(**kwargs), ) ], - css_class=['log-metadata'], + css_classes=['log-metadata'], ) return pg.Html.element( @@ -134,12 +137,23 @@ def render_metadata(): render_message_text(), render_metadata(), ], - css_class=['complex_value'], + css_classes=['complex_value'], + ) + + def _html_tree_view_config(self) -> dict[str, Any]: + return pg.views.HtmlTreeView.get_kwargs( + super()._html_tree_view_config(), + dict( + css_classes=[f'log-{self.level}'], + ) ) - def _html_style(self) -> list[str]: - return super()._html_style() + [ + @classmethod + @functools.cache + def _html_tree_view_css_styles(cls) -> list[str]: + return super()._html_tree_view_css_styles() + [ """ + /* Langfun LogEntry styles. */ .log-time { color: #222; font-size: 12px; @@ -203,9 +217,6 @@ def _html_style(self) -> list[str]: """ ] - def _html_element_class(self) -> Sequence[str] | None: - return super()._html_element_class() + [f'log-{self.level}'] - def log(level: LogLevel, message: str, diff --git a/langfun/core/logging_test.py b/langfun/core/logging_test.py index 17bdeea..de56ace 100644 --- a/langfun/core/logging_test.py +++ b/langfun/core/logging_test.py @@ -69,20 +69,36 @@ def test_html(self): time=time, metadata={} ).to_html(enable_summary_tooltip=False), """ -
12:30:455 + 2 > 3
+
12:30:455 + 2 > 3
""" ) self.assert_html_content( logging.LogEntry( level='error', message='This is a longer message: 5 + 2 > 3', - time=time, metadata=dict(x=1, y=2) + time=time, metadata=dict(x=dict(z=1), y=2) ).to_html( - max_str_len_for_summary=10, + extra_flags=dict( + collapse_log_metadata_level=1, + ), + max_summary_len_for_str=10, enable_summary_tooltip=False, - collapse_log_metadata_level=1 ), """ -
12:30:45This is a ...
This is a longer message: 5 + 2 > 3
+
12:30:45This is a ...
This is a longer message: 5 + 2 > 3
+ """ + ) + self.assert_html_content( + logging.LogEntry( + level='error', message='This is a longer message: 5 + 2 > 3', + time=time, metadata=dict(x=dict(z=1), y=2) + ).to_html( + extra_flags=dict( + max_summary_len_for_str=10, + ), + enable_summary_tooltip=False, + ), + """ +
12:30:45This is a longer message: 5 + 2 > 3
""" ) diff --git a/langfun/core/message.py b/langfun/core/message.py index a8b08de..be07f7a 100644 --- a/langfun/core/message.py +++ b/langfun/core/message.py @@ -14,8 +14,9 @@ """Messages that are exchanged between users and agents.""" import contextlib +import functools import io -from typing import Annotated, Any, Optional, Sequence, Union +from typing import Annotated, Any, Optional, Union from langfun.core import modality from langfun.core import natural_language @@ -506,53 +507,70 @@ def __getattr__(self, key: str) -> Any: v = self.metadata[key] return v.value if isinstance(v, pg.Ref) else v - # pytype: disable=annotation-type-mismatch def _html_tree_view_content( self, *, view: pg.views.HtmlTreeView, root_path: pg.KeyPath, - source_tag: str | Sequence[str] | None = pg.View.PresetArgValue( - ('lm-input', 'lm-output') - ), - include_message_metadata: bool = pg.View.PresetArgValue(True), - collapse_modalities_in_text: bool = pg.View.PresetArgValue(True), - collapse_llm_usage: bool = pg.View.PresetArgValue(False), - collapse_message_result_level: int | None = pg.View.PresetArgValue(1), - collapse_message_metadata_level: int | None = pg.View.PresetArgValue(0), - collapse_source_message_level: int | None = pg.View.PresetArgValue(1), - collapse_level: int | None = pg.View.PresetArgValue(1), + collapse_level: int | None = None, + extra_flags: dict[str, Any] | None = None, **kwargs, ) -> pg.Html: - # pytype: enable=annotation-type-mismatch """Returns the HTML representation of the message. Args: view: The HTML tree view. root_path: The root path of the message. - source_tag: tags to filter source messages. If None, the entire - source chain will be included. - include_message_metadata: Whether to include the metadata of the message. - collapse_modalities_in_text: Whether to collapse the modalities in the - message text. - collapse_llm_usage: Whether to collapse the usage in the message. - collapse_message_result_level: The level to collapse the result in the - message. - collapse_message_metadata_level: The level to collapse the metadata in the - message. - collapse_source_message_level: The level to collapse the source in the - message. collapse_level: The global collapse level. - **kwargs: Other keyword arguments. + extra_flags: Extra flags to control the rendering. + - source_tag: tags to filter source messages. If None, the entire + source chain will be included. + - include_message_metadata: Whether to include the metadata of the + message. + - collapse_modalities_in_text: Whether to collapse the modalities in the + message text. + - collapse_llm_usage: Whether to collapse the usage in the message. + - collapse_message_result_level: The level to collapse the result in the + message. + - collapse_message_metadata_level: The level to collapse the metadata in + the message. + - collapse_source_message_level: The level to collapse the source in the + message. + - collapse_level: The global collapse level. + **kwargs: Omitted keyword arguments. Returns: The HTML representation of the message content. """ + extra_flags = extra_flags if extra_flags is not None else {} + + include_message_metadata: bool = extra_flags.get( + 'include_message_metadata', True + ) + source_tag: str | tuple[str, ...] | None = extra_flags.get( + 'source_tag', ('lm-input', 'lm-output') + ) + collapse_modalities_in_text: bool = extra_flags.get( + 'collapse_modalities_in_text', True + ) + collapse_llm_usage: bool = extra_flags.get( + 'collapse_llm_usage', False + ) + collapse_message_result_level: int | None = extra_flags.get( + 'collapse_message_result_level', 1 + ) + collapse_message_metadata_level: int | None = extra_flags.get( + 'collapse_message_metadata_level', 1 + ) + collapse_source_message_level: int | None = extra_flags.get( + 'collapse_source_message_level', 1 + ) + passthrough_kwargs = view.get_passthrough_kwargs(**kwargs) def render_tags(): return pg.Html.element( 'div', [pg.Html.element('span', [tag]) for tag in self.tags], - css_class=['message-tags'], + css_classes=['message-tags'], ) def render_message_text(): @@ -573,12 +591,16 @@ def render_message_text(): chunk, name=chunk.referred_name, root_path=child_path, - collapse_level=child_path.depth + ( + collapse_level=( 0 if collapse_modalities_in_text else 1 - ) + ), + extra_flags=dict( + display_modality_when_hover=True, + ), + **passthrough_kwargs, ) ], - css_class=['modality-in-text'], + css_classes=['modality-in-text'], ) ) referred_chunks[chunk.referred_name] = chunk @@ -596,14 +618,15 @@ def render_result(): self.result, name='result', root_path=child_path, - collapse_level=view.max_collapse_level( - collapse_level, + collapse_level=view.get_collapse_level( + (collapse_level, -1), collapse_message_result_level, - child_path, - ) + ), + extra_flags=extra_flags, + **passthrough_kwargs, ) ], - css_class=['message-result'], + css_classes=['message-result'], ) def render_usage(): @@ -616,15 +639,19 @@ def render_usage(): view.render( self.usage, name='llm usage', + key_style='label', root_path=child_path, - collapse_level=view.max_collapse_level( - collapse_level, + collapse_level=view.get_collapse_level( + (collapse_level, -1), 0 if collapse_llm_usage else 1, - child_path, - ) + ), + extra_flags=extra_flags, + **view.get_passthrough_kwargs( + remove=['key_style'], **kwargs + ), ) ], - css_class=['message-usage'], + css_classes=['message-usage'], ) def render_source_message(): @@ -635,21 +662,22 @@ def render_source_message(): source = source.source if source is not None: child_path = root_path + 'source' + child_extra_flags = extra_flags.copy() + child_extra_flags['collapse_source_message_level'] = ( + view.get_collapse_level( + (collapse_source_message_level, -1), 0, + ) + ) return view.render( self.source, name='source', root_path=child_path, - include_metadata=include_message_metadata, - collapse_level=view.max_collapse_level( - collapse_level, + collapse_level=view.get_collapse_level( + (collapse_level, -1), collapse_source_message_level, - child_path ), - collapse_source_level=max(0, collapse_source_message_level - 1), - collapse_modalities=collapse_modalities_in_text, - collapse_usage=collapse_llm_usage, - collapse_metadata_level=collapse_message_metadata_level, - collapse_result_level=collapse_message_result_level, + extra_flags=child_extra_flags, + **passthrough_kwargs, ) return None @@ -662,17 +690,20 @@ def render_metadata(): [ view.render( self.metadata, - css_class=['message-metadata'], + css_classes=['message-metadata'], + exclude_keys=['usage', 'result'], name='metadata', root_path=child_path, - collapse_level=view.max_collapse_level( - collapse_level, + collapse_level=view.get_collapse_level( + (collapse_level, -1), collapse_message_metadata_level, - child_path, - ) + ), + **view.get_passthrough_kwargs( + remove=['exclude_keys'], **kwargs + ), ) ], - css_class=['message-metadata'], + css_classes=['message-metadata'], ) return pg.Html.element( @@ -685,18 +716,30 @@ def render_metadata(): render_metadata(), render_source_message(), ], - css_class=['complex_value'], + css_classes=['complex_value'], + ) + + @classmethod + @functools.cache + def _html_tree_view_config(cls) -> dict[str, Any]: + return pg.views.HtmlTreeView.get_kwargs( + super()._html_tree_view_config(), + dict( + css_classes=['lf-message'], + ) ) - def _html_style(self) -> list[str]: - return super()._html_style() + [ + @classmethod + @functools.cache + def _html_tree_view_css_styles(cls) -> list[str]: + return super()._html_tree_view_css_styles() + [ """ /* Langfun Message styles.*/ [class^="message-"] > details { margin: 0px 0px 5px 0px; border: 1px solid #EEE; } - details.lf-message > summary > .summary_title::after { + .lf-message.summary-title::after { content: ' 💬'; } details.pyglove.ai-message { @@ -739,12 +782,12 @@ def _html_style(self) -> list[str]: margin: 0px 5px 0px 5px; } .message-result { - color: purple; + color: dodgerblue; } .message-usage { color: orange; } - .message-usage .object_key.str { + .message-usage .object-key.str { border: 1px solid orange; background-color: orange; color: white; @@ -752,9 +795,6 @@ def _html_style(self) -> list[str]: """ ] - def _html_element_class(self) -> list[str]: - return super()._html_element_class() + ['lf-message'] - # # Messages of different roles. diff --git a/langfun/core/message_test.py b/langfun/core/message_test.py index 0177d4f..d263d46 100644 --- a/langfun/core/message_test.py +++ b/langfun/core/message_test.py @@ -345,13 +345,80 @@ def assert_html_content(self, html, expected): print(actual) self.assertEqual(actual, expected) + def test_html_style(self): + self.assertIn( + inspect.cleandoc( + """ + /* Langfun Message styles.*/ + [class^="message-"] > details { + margin: 0px 0px 5px 0px; + border: 1px solid #EEE; + } + .lf-message.summary-title::after { + content: ' 💬'; + } + details.pyglove.ai-message { + border: 1px solid blue; + color: blue; + } + details.pyglove.user-message { + border: 1px solid green; + color: green; + } + .message-tags { + margin: 5px 0px 5px 0px; + font-size: .8em; + } + .message-tags > span { + border-radius: 5px; + background-color: #CCC; + padding: 3px; + margin: 0px 2px 0px 2px; + color: white; + } + .message-text { + padding: 20px; + margin: 10px 5px 10px 5px; + font-style: italic; + font-size: 1.1em; + white-space: pre-wrap; + border: 1px solid #EEE; + border-radius: 5px; + background-color: #EEE; + } + .modality-in-text { + display: inline-block; + } + .modality-in-text > details { + display: inline-block; + font-size: 0.8em; + border: 0; + background-color: #A6F1A6; + margin: 0px 5px 0px 5px; + } + .message-result { + color: dodgerblue; + } + .message-usage { + color: orange; + } + .message-usage .object-key.str { + border: 1px solid orange; + background-color: orange; + color: white; + } + """ + ), + message.UserMessage('hi').to_html().style_section, + ) + def test_html_user_message(self): self.assert_html_content( message.UserMessage( 'what is a
' ).to_html(enable_summary_tooltip=False), """ -
UserMessage(...)
what is a <div>
+
UserMessage(...)
what is a <div>
""" ) self.assert_html_content( @@ -359,9 +426,12 @@ def test_html_user_message(self): 'what is this <<[[image]]>>', tags=['lm-input'], image=CustomModality('bird') - ).to_html(enable_summary_tooltip=False, include_message_metadata=False), + ).to_html( + enable_summary_tooltip=False, + extra_flags=dict(include_message_metadata=False) + ), """ -
UserMessage(...)
lm-input
what is this
image
CustomModality(...)
contentmetadata.image.content
'bird'
+
UserMessage(...)
lm-input
what is this
imagemetadata.image
CustomModality(...)
contentmetadata.image.content
str
'bird'
""" ) @@ -385,21 +455,41 @@ def test_html_ai_message(self): self.assert_html_content( ai_message.to_html(enable_summary_tooltip=False), """ -
AIMessage(...)
lm-responselm-output
My name is Gemini
result
Dict(...)
xmetadata.result.x
1
ymetadata.result.y
2
zmetadata.result.z
Dict(...)
ametadata.result.z.a
List(...)
0metadata.result.z.a[0]
12
1metadata.result.z.a[1]
323
llm usage
LMSamplingUsage(...)
prompt_tokensmetadata.usage.prompt_tokens
10
completion_tokensmetadata.usage.completion_tokens
2
total_tokensmetadata.usage.total_tokens
12
num_requestsmetadata.usage.num_requests
1
estimated_costmetadata.usage.estimated_cost
None
source
UserMessage(...)
lm-input
What is in this image?
image
CustomModality(...)
contentsource.metadata.image.content
'foo'
this is a test
+
AIMessage(...)
lm-responselm-output
My name is Gemini
resultmetadata.result
Dict(...)
xmetadata.result.x
int
1
ymetadata.result.y
int
2
zmetadata.result.z
Dict(...)
ametadata.result.z.a
List(...)
0metadata.result.z.a[0]12
1metadata.result.z.a[1]323
llm usagemetadata.usage
LMSamplingUsage(...)
prompt_tokensmetadata.usage.prompt_tokens10
completion_tokensmetadata.usage.completion_tokens2
total_tokensmetadata.usage.total_tokens12
num_requestsmetadata.usage.num_requests1
estimated_costmetadata.usage.estimated_costNone
sourcesource
UserMessage(...)
lm-input
What is in this image?
imagesource.metadata.image
CustomModality(...)
contentsource.metadata.image.content
str
'foo'
this is a test
+ """ + ) + self.assert_html_content( + ai_message.to_html( + key_style='label', + enable_summary_tooltip=False, + extra_flags=dict( + collapse_modalities_in_text=False, + collapse_llm_usage=True, + collapse_message_result_level=0, + collapse_message_metadata_level=0, + collapse_source_message_level=0, + source_tag=None, + ), + ), + """ +
AIMessage(...)
lm-responselm-output
My name is Gemini
resultmetadata.result
Dict(...)
xmetadata.result.x1
ymetadata.result.y2
zmetadata.result.z
Dict(...)
ametadata.result.z.a
List(...)
0metadata.result.z.a[0]12
1metadata.result.z.a[1]323
llm usagemetadata.usage
LMSamplingUsage(...)
prompt_tokensmetadata.usage.prompt_tokens10
completion_tokensmetadata.usage.completion_tokens2
total_tokensmetadata.usage.total_tokens12
num_requestsmetadata.usage.num_requests1
estimated_costmetadata.usage.estimated_costNone
sourcesource
UserMessage(...)
lm-input
What is in this image?
imagesource.metadata.image
CustomModality(...)
contentsource.metadata.image.content'foo'
this is a test
sourcesource.source
UserMessage(...)
User input
""" ) self.assert_html_content( ai_message.to_html( + key_style='label', enable_summary_tooltip=False, - collapse_modalities_in_text=False, - collapse_llm_usage=True, - collapse_message_result_level=0, - collapse_message_metadata_level=0, - collapse_source_message_level=0, - source_tag=None, + extra_flags=dict( + collapse_modalities_in_text=True, + collapse_llm_usage=False, + collapse_message_result_level=1, + collapse_message_metadata_level=1, + collapse_source_message_level=2, + source_tag=None, + ), ), """ -
AIMessage(...)
lm-responselm-output
My name is Gemini
result
Dict(...)
xmetadata.result.x
1
ymetadata.result.y
2
zmetadata.result.z
Dict(...)
ametadata.result.z.a
List(...)
0metadata.result.z.a[0]
12
1metadata.result.z.a[1]
323
llm usage
LMSamplingUsage(...)
prompt_tokensmetadata.usage.prompt_tokens
10
completion_tokensmetadata.usage.completion_tokens
2
total_tokensmetadata.usage.total_tokens
12
num_requestsmetadata.usage.num_requests
1
estimated_costmetadata.usage.estimated_cost
None
source
UserMessage(...)
lm-input
What is in this image?
image
CustomModality(...)
contentsource.metadata.image.content
'foo'
this is a test
source
UserMessage(...)
User input
+
AIMessage(...)
lm-responselm-output
My name is Gemini
resultmetadata.result
Dict(...)
xmetadata.result.x1
ymetadata.result.y2
zmetadata.result.z
Dict(...)
ametadata.result.z.a
List(...)
0metadata.result.z.a[0]12
1metadata.result.z.a[1]323
llm usagemetadata.usage
LMSamplingUsage(...)
prompt_tokensmetadata.usage.prompt_tokens10
completion_tokensmetadata.usage.completion_tokens2
total_tokensmetadata.usage.total_tokens12
num_requestsmetadata.usage.num_requests1
estimated_costmetadata.usage.estimated_costNone
sourcesource
UserMessage(...)
lm-input
What is in this image?
imagesource.metadata.image
CustomModality(...)
contentsource.metadata.image.content'foo'
this is a test
sourcesource.source
UserMessage(...)
User input
""" ) diff --git a/langfun/core/modalities/mime.py b/langfun/core/modalities/mime.py index 55a5d43..6bb6bbc 100644 --- a/langfun/core/modalities/mime.py +++ b/langfun/core/modalities/mime.py @@ -15,7 +15,7 @@ import base64 import functools -from typing import Annotated, Iterable, Type, Union +from typing import Annotated, Any, Iterable, Type, Union import langfun.core as lf import pyglove as pg import requests @@ -183,35 +183,47 @@ def _html_tree_view_content( **kwargs) -> str: return self._raw_html() - def _html_tree_view_render( + def _html_tree_view( self, view: pg.views.HtmlTreeView, - raw_mime_content: bool = pg.View.PresetArgValue(False), # pytype: disable=annotation-type-mismatch - display_modality_when_hover: bool = pg.View.PresetArgValue(False), # pytype: disable=annotation-type-mismatch + extra_flags: dict[str, Any] | None = None, **kwargs ): + extra_flags = extra_flags if extra_flags is not None else {} + raw_mime_content = extra_flags.get('raw_mime_content', False) + display_modality_when_hover = extra_flags.get( + 'display_modality_when_hover', False + ) if raw_mime_content: - return pg.Html(self._raw_html()) - else: - if display_modality_when_hover: - kwargs.update( - display_modality_when_hover=True, - enable_summary_tooltip=True, - ) - return super()._html_tree_view_render(view=view, **kwargs) - - def _html_tree_view_tooltip( + kwargs['enable_summary'] = False + elif display_modality_when_hover: + kwargs.update( + enable_summary=True, + enable_summary_tooltip=True, + ) + return super()._html_tree_view( + view=view, extra_flags=extra_flags, **kwargs + ) + + def _html_tree_view_summary( self, *, view: pg.views.HtmlTreeView, - content: pg.Html | str | None = None, - display_modality_when_hover: bool = pg.View.PresetArgValue(False), # pytype: disable=annotation-type-mismatch + extra_flags: dict[str, Any] | None = None, **kwargs ): - if content is None and display_modality_when_hover: - content = self._raw_html() - return super()._html_tree_view_tooltip( - view=view, content=content, **kwargs + extra_flags = extra_flags or {} + if extra_flags.get('display_modality_when_hover', False): + def summary_tooltip(*args, content: str | None = None, **kwargs): + del content + return view.tooltip(*args, content=self._raw_html(), **kwargs) + else: + summary_tooltip = None + return super()._html_tree_view_summary( + view=view, + summary_tooltip_fn=summary_tooltip, + extra_flags=extra_flags, + **kwargs ) def _raw_html(self) -> str: diff --git a/langfun/core/modalities/mime_test.py b/langfun/core/modalities/mime_test.py index 369678d..8d92405 100644 --- a/langfun/core/modalities/mime_test.py +++ b/langfun/core/modalities/mime_test.py @@ -92,14 +92,16 @@ def test_html(self): enable_key_tooltip=False, ), """ -
Custom(...)
+
Custom(...)
""" ) self.assert_html_content( mime.Custom('text/plain', b'foo').to_html( enable_summary_tooltip=False, enable_key_tooltip=False, - raw_mime_content=True, + extra_flags=dict( + raw_mime_content=True, + ) ), """ @@ -109,10 +111,12 @@ def test_html(self): mime.Custom('text/plain', b'foo').to_html( enable_summary_tooltip=False, enable_key_tooltip=False, - display_modality_when_hover=True, + extra_flags=dict( + display_modality_when_hover=True, + ) ), """ -
Custom(...)
+
Custom(...)
""" ) diff --git a/langfun/core/structured/mapping.py b/langfun/core/structured/mapping.py index 5bcb2d3..59d2c1a 100644 --- a/langfun/core/structured/mapping.py +++ b/langfun/core/structured/mapping.py @@ -13,6 +13,7 @@ # limitations under the License. """The base of symbolic mapping methods.""" +import functools import io from typing import Annotated, Any, Callable import langfun.core as lf @@ -183,43 +184,59 @@ def natural_language_format(self) -> str: result.write(lf.colored(str(self.metadata), color='cyan')) return result.getvalue().strip() - def _html_tree_view_content( - self, - *, - parent: Any, - view: pg.views.HtmlTreeView, - root_path: pg.KeyPath, - **kwargs, - ): - def render_value(value, **kwargs): + @classmethod + @functools.cache + def _html_tree_view_config(cls) -> dict[str, Any]: + + def render_value(view, *, value, **kwargs): if isinstance(value, lf.Template): # Make a shallow copy to make sure modalities are rooted by # the input. value = value.clone().render() + if value is None: + return None return view.render(value, **kwargs) - exclude_keys = [] - if not self.context: - exclude_keys.append('context') - if not self.schema: - exclude_keys.append('schema') - if not self.metadata: - exclude_keys.append('metadata') - - kwargs.pop('special_keys', None) - kwargs.pop('exclude_keys', None) - return view.complex_value( - self.sym_init_args, - parent=self, - root_path=root_path, - render_value_fn=render_value, - special_keys=['input', 'output', 'context', 'schema', 'metadata'], - exclude_keys=exclude_keys, - **kwargs + return pg.views.HtmlTreeView.get_kwargs( + super()._html_tree_view_config(), + dict( + include_keys=['input', 'output', 'context', 'schema', 'metadata'], + extra_flags=dict( + render_value_fn=render_value, + ), + child_config=dict( + input=dict( + collapse_level=1, + ), + output=dict( + css_classes=['lf-example-output'], + collapse_level=1, + ), + schema=dict( + css_classes=['lf-example-schema'], + collapse_level=1, + ), + metadata=dict( + css_classes=['lf-example-metadata'], + collapse_level=1, + ), + ), + ) ) - def _html_tree_view_uncollapse_level(self) -> int: - return 2 + @classmethod + @functools.cache + def _html_tree_view_css_styles(cls) -> list[str]: + return super()._html_tree_view_css_styles() + [ + """ + .lf-example-output { + color: dodgerblue; + } + .lf-example-schema { + color: blue; + } + """ + ] class Mapping(lf.LangFunc): diff --git a/langfun/core/structured/mapping_test.py b/langfun/core/structured/mapping_test.py index 3d23d96..81a8823 100644 --- a/langfun/core/structured/mapping_test.py +++ b/langfun/core/structured/mapping_test.py @@ -194,15 +194,18 @@ class Addition(lf.Template): ) self.assert_html_content( example.to_html( - enable_summary_tooltip=False, include_message_metadata=False + enable_summary_tooltip=False, + extra_flags=dict( + include_message_metadata=False + ) ), """ -
MappingExample(...)
input
UserMessage(...)
rendered
1 + 2 = ?
output
Answer(...)
answeroutput.answer
3
context
'compute 1 + 1'
'compute 1 + 1'
schema
Schema(...)
Answer +
MappingExample(...)
inputinput
UserMessage(...)
rendered
1 + 2 = ?
outputoutput
Answer(...)
answeroutput.answer
int
3
contextcontext
str
'compute 1 + 1'
schemaschema
Schema(...)
Answer ```python class Answer: answer: int - ```
metadata
Dict(...)
foometadata.foo
'bar'
+ ```
""" ) @@ -212,10 +215,13 @@ class Answer: ) self.assert_html_content( example.to_html( - enable_summary_tooltip=False, include_message_metadata=False + enable_summary_tooltip=False, + extra_flags=dict( + include_message_metadata=False + ) ), """ -
MappingExample(...)
input
UserMessage(...)
rendered
1 + 2 = ?
output
Answer(...)
answeroutput.answer
3
+
MappingExample(...)
inputinput
UserMessage(...)
rendered
1 + 2 = ?
outputoutput
Answer(...)
answeroutput.answer
int
3
schemaschema
ContextualAttribute(...)
None
""" ) diff --git a/langfun/core/structured/schema.py b/langfun/core/structured/schema.py index 92bccef..031285f 100644 --- a/langfun/core/structured/schema.py +++ b/langfun/core/structured/schema.py @@ -199,7 +199,7 @@ def _html_tree_view_content( return pg.Html.element( 'div', [self.schema_str(protocol='python')], - css_class=['lf-schema-definition'] + css_classes=['lf-schema-definition'] ).add_style( """ .lf-schema-definition { diff --git a/langfun/core/template.py b/langfun/core/template.py index e640375..82bde38 100644 --- a/langfun/core/template.py +++ b/langfun/core/template.py @@ -17,7 +17,7 @@ import dataclasses import functools import inspect -from typing import Annotated, Any, Callable, Iterator, Sequence, Set, Tuple, Type, Union +from typing import Annotated, Any, Callable, Iterator, Set, Tuple, Type, Union import jinja2 from jinja2 import meta as jinja2_meta @@ -531,59 +531,48 @@ def _html_tree_view_content( *, view: pg.views.HtmlTreeView, root_path: pg.KeyPath, - collapse_template_vars_level: int | None = pg.View.PresetArgValue(1), - collapse_level: int | None = pg.View.PresetArgValue(1), # pytype: disable=annotation-type-mismatch + collapse_level: int | None = None, + extra_flags: dict[str, Any] | None = None, + debug: bool = False, **kwargs, ): + extra_flags = extra_flags if extra_flags is not None else {} + collapse_template_vars_level: int | None = extra_flags.get( + 'collapse_template_vars_level', 1 + ) + def render_template_str(): return pg.Html.element( 'div', [ pg.Html.element('span', [self.template_str]) ], - css_class=['template-str'], + css_classes=['template-str'], ) def render_fields(): - def render_value_fn(value, *, root_path, **kwargs): - if isinstance(value, component.ContextualAttribute): - inferred = self.sym_inferred(root_path.key, pg.MISSING_VALUE) - if inferred != pg.MISSING_VALUE: - return pg.Html.element( - 'div', - [ - view.render(inferred, root_path=root_path, **kwargs) - ], - css_class=['inferred-value'], - ) - else: - return pg.Html.element( - 'span', - ['(external)'], - css_class=['contextual-variable'], - ) - return view.render( - value, root_path=root_path, **kwargs - ) return pg.Html.element( 'fieldset', [ pg.Html.element('legend', ['Template Variables']), view.complex_value( - self.sym_init_args, + {k: v for k, v in self.sym_items()}, name='fields', root_path=root_path, - render_value_fn=render_value_fn, - exclude_keys=['template_str', 'clean'], parent=self, - collapse_level=view.max_collapse_level( - collapse_level, - collapse_template_vars_level, - root_path - ), + exclude_keys=['template_str', 'clean'], + collapse_level=max( + collapse_template_vars_level, collapse_level + ) if collapse_level is not None else None, + extra_flags=extra_flags, + debug=debug, + **view.get_passthrough_kwargs( + remove=['exclude_keys'], + **kwargs, + ) ), ], - css_class=['template-fields'], + css_classes=['template-fields'], ) return pg.Html.element( @@ -592,11 +581,12 @@ def render_value_fn(value, *, root_path, **kwargs): render_template_str(), render_fields(), ], - css_class=['complex_value'], + css_classes=['complex_value'], ) - def _html_style(self) -> list[str]: - return super()._html_style() + [ + @classmethod + def _html_tree_view_css_styles(cls) -> list[str]: + return super()._html_tree_view_css_styles() + [ """ /* Langfun Template styles. */ .template-str { @@ -619,22 +609,19 @@ def _html_style(self) -> list[str]: font-size: 0.8em; margin: 5px 0px 5px 0px; } - .inferred-value::after { - content: ' (inferred)'; - color: gray; - font-style: italic; - } - .contextual-variable { - margin: 0px 0px 0px 5px; - font-style: italic; - color: gray; - } """ ] - # Additional CSS class to add to the root
element. - def _html_element_class(self) -> Sequence[str] | None: - return super()._html_element_class() + ['lf-template'] + @classmethod + @functools.cache + def _html_tree_view_config(cls) -> dict[str, Any]: + return pg.views.HtmlTreeView.get_kwargs( + super()._html_tree_view_config(), + dict( + css_classes=['lf-template'], + ) + ) + # Register converter from str to LangFunc, therefore we can always # pass strs to attributes that accept LangFunc. diff --git a/langfun/core/template_test.py b/langfun/core/template_test.py index d490f48..c141df1 100644 --- a/langfun/core/template_test.py +++ b/langfun/core/template_test.py @@ -579,12 +579,50 @@ class Bar(Template): """ y: Any + self.assertIn( + inspect.cleandoc( + """ + /* Langfun Template styles. */ + .template-str { + padding: 10px; + margin: 10px 5px 10px 5px; + font-style: italic; + font-size: 1.1em; + white-space: pre-wrap; + border: 1px solid #EEE; + border-radius: 5px; + background-color: #EEE; + color: #cc2986; + } + .template-fields { + margin: 0px 0px 5px 0px; + border: 1px solid #EEE; + padding: 5px; + } + .template-fields > legend { + font-size: 0.8em; + margin: 5px 0px 5px 0px; + } + """ + ), + Foo(x=1, y=2).to_html().style_section, + ) + self.assert_html_content( + Foo(x=Bar('{{y}} + {{z}}'), y=1).to_html( + enable_summary_tooltip=False, + ), + """ +
Foo(...)
{{x}} + {{y}} = ?
Template Variables
xx
Bar(...)
{{y}} + {{z}}
Template Variables
yx.y
ContextualAttribute(...)
1
zx.z
ContextualAttribute(...)
(not available)
yy
int
1
+ """ + ) self.assert_html_content( Foo(x=Bar('{{y}} + {{z}}'), y=1).to_html( enable_summary_tooltip=False, + collapse_level=0, + key_style='label', ), """ -
Foo(...)
{{x}} + {{y}} = ?
Template Variables
xx
Bar(...)
{{y}} + {{z}}
Template Variables
yx.y
1
zx.z
(external)
yy
1
+
Foo(...)
{{x}} + {{y}} = ?
Template Variables
xx
Bar(...)
{{y}} + {{z}}
Template Variables
yx.y
ContextualAttribute(...)
1
zx.z
ContextualAttribute(...)
(not available)
yy1
""" )