langfun 0.0.2.dev20240429__py3-none-any.whl → 0.1.2.dev202501140804__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.
Files changed (144) hide show
  1. langfun/__init__.py +20 -2
  2. langfun/core/__init__.py +16 -5
  3. langfun/core/agentic/__init__.py +30 -0
  4. langfun/core/agentic/action.py +854 -0
  5. langfun/core/agentic/action_eval.py +150 -0
  6. langfun/core/agentic/action_eval_test.py +109 -0
  7. langfun/core/agentic/action_test.py +136 -0
  8. langfun/core/coding/python/__init__.py +5 -11
  9. langfun/core/coding/python/correction.py +37 -21
  10. langfun/core/coding/python/correction_test.py +29 -3
  11. langfun/core/coding/python/execution.py +40 -216
  12. langfun/core/coding/python/execution_test.py +29 -89
  13. langfun/core/coding/python/generation.py +21 -11
  14. langfun/core/coding/python/generation_test.py +2 -2
  15. langfun/core/coding/python/parsing.py +108 -193
  16. langfun/core/coding/python/parsing_test.py +2 -105
  17. langfun/core/component.py +63 -2
  18. langfun/core/component_test.py +53 -0
  19. langfun/core/concurrent.py +414 -117
  20. langfun/core/concurrent_test.py +111 -24
  21. langfun/core/console.py +18 -5
  22. langfun/core/console_test.py +17 -0
  23. langfun/core/eval/__init__.py +16 -1
  24. langfun/core/eval/base.py +622 -174
  25. langfun/core/eval/base_test.py +200 -54
  26. langfun/core/eval/matching.py +63 -76
  27. langfun/core/eval/matching_test.py +17 -8
  28. langfun/core/eval/patching.py +130 -0
  29. langfun/core/eval/patching_test.py +170 -0
  30. langfun/core/eval/scoring.py +26 -26
  31. langfun/core/eval/scoring_test.py +19 -2
  32. langfun/core/eval/v2/__init__.py +42 -0
  33. langfun/core/eval/v2/checkpointing.py +380 -0
  34. langfun/core/eval/v2/checkpointing_test.py +228 -0
  35. langfun/core/eval/v2/eval_test_helper.py +136 -0
  36. langfun/core/eval/v2/evaluation.py +725 -0
  37. langfun/core/eval/v2/evaluation_test.py +180 -0
  38. langfun/core/eval/v2/example.py +305 -0
  39. langfun/core/eval/v2/example_test.py +128 -0
  40. langfun/core/eval/v2/experiment.py +1048 -0
  41. langfun/core/eval/v2/experiment_test.py +433 -0
  42. langfun/core/eval/v2/metric_values.py +156 -0
  43. langfun/core/eval/v2/metric_values_test.py +80 -0
  44. langfun/core/eval/v2/metrics.py +357 -0
  45. langfun/core/eval/v2/metrics_test.py +203 -0
  46. langfun/core/eval/v2/progress.py +348 -0
  47. langfun/core/eval/v2/progress_test.py +82 -0
  48. langfun/core/eval/v2/progress_tracking.py +210 -0
  49. langfun/core/eval/v2/progress_tracking_test.py +66 -0
  50. langfun/core/eval/v2/reporting.py +270 -0
  51. langfun/core/eval/v2/reporting_test.py +158 -0
  52. langfun/core/eval/v2/runners.py +488 -0
  53. langfun/core/eval/v2/runners_test.py +334 -0
  54. langfun/core/langfunc.py +4 -17
  55. langfun/core/langfunc_test.py +22 -6
  56. langfun/core/language_model.py +577 -39
  57. langfun/core/language_model_test.py +470 -56
  58. langfun/core/llms/__init__.py +87 -16
  59. langfun/core/llms/anthropic.py +312 -87
  60. langfun/core/llms/anthropic_test.py +71 -3
  61. langfun/core/llms/cache/base.py +21 -2
  62. langfun/core/llms/cache/in_memory.py +13 -0
  63. langfun/core/llms/cache/in_memory_test.py +53 -2
  64. langfun/core/llms/compositional.py +101 -0
  65. langfun/core/llms/compositional_test.py +73 -0
  66. langfun/core/llms/deepseek.py +117 -0
  67. langfun/core/llms/deepseek_test.py +61 -0
  68. langfun/core/llms/fake.py +11 -7
  69. langfun/core/llms/fake_test.py +14 -0
  70. langfun/core/llms/gemini.py +507 -0
  71. langfun/core/llms/gemini_test.py +195 -0
  72. langfun/core/llms/google_genai.py +62 -218
  73. langfun/core/llms/google_genai_test.py +9 -202
  74. langfun/core/llms/groq.py +160 -144
  75. langfun/core/llms/groq_test.py +31 -137
  76. langfun/core/llms/llama_cpp.py +15 -42
  77. langfun/core/llms/llama_cpp_test.py +4 -30
  78. langfun/core/llms/openai.py +395 -203
  79. langfun/core/llms/openai_compatible.py +179 -0
  80. langfun/core/llms/openai_compatible_test.py +495 -0
  81. langfun/core/llms/openai_test.py +30 -395
  82. langfun/core/llms/rest.py +113 -0
  83. langfun/core/llms/rest_test.py +111 -0
  84. langfun/core/llms/vertexai.py +192 -0
  85. langfun/core/llms/vertexai_test.py +52 -0
  86. langfun/core/logging.py +284 -0
  87. langfun/core/logging_test.py +125 -0
  88. langfun/core/message.py +319 -9
  89. langfun/core/message_test.py +190 -13
  90. langfun/core/modalities/__init__.py +6 -2
  91. langfun/core/modalities/audio.py +30 -0
  92. langfun/core/modalities/audio_test.py +63 -0
  93. langfun/core/modalities/image.py +39 -20
  94. langfun/core/modalities/image_test.py +52 -9
  95. langfun/core/modalities/mime.py +206 -29
  96. langfun/core/modalities/mime_test.py +90 -9
  97. langfun/core/modalities/ms_office.py +117 -0
  98. langfun/core/modalities/ms_office_test.py +389 -0
  99. langfun/core/modalities/pdf.py +22 -0
  100. langfun/core/modalities/pdf_test.py +57 -0
  101. langfun/core/modalities/video.py +9 -26
  102. langfun/core/modalities/video_test.py +3 -3
  103. langfun/core/modality.py +26 -3
  104. langfun/core/modality_test.py +2 -2
  105. langfun/core/sampling.py +11 -11
  106. langfun/core/structured/__init__.py +12 -16
  107. langfun/core/structured/completion.py +32 -5
  108. langfun/core/structured/completion_test.py +7 -6
  109. langfun/core/structured/description.py +2 -2
  110. langfun/core/structured/description_test.py +3 -3
  111. langfun/core/structured/function_generation.py +60 -27
  112. langfun/core/structured/function_generation_test.py +72 -2
  113. langfun/core/structured/mapping.py +97 -47
  114. langfun/core/structured/mapping_test.py +90 -2
  115. langfun/core/structured/parsing.py +33 -21
  116. langfun/core/structured/parsing_test.py +53 -9
  117. langfun/core/structured/querying.py +746 -0
  118. langfun/core/structured/{prompting_test.py → querying_test.py} +469 -51
  119. langfun/core/structured/schema.py +204 -97
  120. langfun/core/structured/schema_generation.py +1 -1
  121. langfun/core/structured/schema_test.py +130 -29
  122. langfun/core/structured/scoring.py +125 -19
  123. langfun/core/structured/scoring_test.py +30 -0
  124. langfun/core/structured/tokenization.py +64 -0
  125. langfun/core/structured/tokenization_test.py +48 -0
  126. langfun/core/template.py +115 -1
  127. langfun/core/template_test.py +71 -1
  128. langfun/core/templates/conversation.py +9 -0
  129. langfun/core/templates/conversation_test.py +4 -3
  130. langfun/core/templates/selfplay_test.py +10 -2
  131. langfun-0.1.2.dev202501140804.dist-info/METADATA +225 -0
  132. langfun-0.1.2.dev202501140804.dist-info/RECORD +153 -0
  133. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/WHEEL +1 -1
  134. langfun/core/coding/python/errors.py +0 -108
  135. langfun/core/coding/python/errors_test.py +0 -99
  136. langfun/core/coding/python/permissions.py +0 -90
  137. langfun/core/coding/python/permissions_test.py +0 -86
  138. langfun/core/structured/prompting.py +0 -238
  139. langfun/core/text_formatting.py +0 -162
  140. langfun/core/text_formatting_test.py +0 -47
  141. langfun-0.0.2.dev20240429.dist-info/METADATA +0 -100
  142. langfun-0.0.2.dev20240429.dist-info/RECORD +0 -108
  143. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/LICENSE +0 -0
  144. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,192 @@
1
+ # Copyright 2023 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Vertex AI generative models."""
15
+
16
+ import functools
17
+ import os
18
+ from typing import Annotated, Any
19
+
20
+ import langfun.core as lf
21
+ from langfun.core.llms import gemini
22
+ import pyglove as pg
23
+
24
+ try:
25
+ # pylint: disable=g-import-not-at-top
26
+ from google import auth as google_auth
27
+ from google.auth import credentials as credentials_lib
28
+ from google.auth.transport import requests as auth_requests
29
+ # pylint: enable=g-import-not-at-top
30
+
31
+ Credentials = credentials_lib.Credentials
32
+ except ImportError:
33
+ google_auth = None
34
+ credentials_lib = None
35
+ auth_requests = None
36
+ Credentials = Any
37
+
38
+
39
+ @lf.use_init_args(['model'])
40
+ @pg.members([('api_endpoint', pg.typing.Str().freeze(''))])
41
+ class VertexAI(gemini.Gemini):
42
+ """Language model served on VertexAI with REST API."""
43
+
44
+ project: Annotated[
45
+ str | None,
46
+ (
47
+ 'Vertex AI project ID. Or set from environment variable '
48
+ 'VERTEXAI_PROJECT.'
49
+ ),
50
+ ] = None
51
+
52
+ location: Annotated[
53
+ str | None,
54
+ (
55
+ 'Vertex AI service location. Or set from environment variable '
56
+ 'VERTEXAI_LOCATION.'
57
+ ),
58
+ ] = None
59
+
60
+ credentials: Annotated[
61
+ Credentials | None,
62
+ (
63
+ 'Credentials to use. If None, the default credentials to the '
64
+ 'environment will be used.'
65
+ ),
66
+ ] = None
67
+
68
+ def _on_bound(self):
69
+ super()._on_bound()
70
+ if google_auth is None:
71
+ raise ValueError(
72
+ 'Please install "langfun[llm-google-vertex]" to use Vertex AI models.'
73
+ )
74
+ self._project = None
75
+ self._credentials = None
76
+
77
+ def _initialize(self):
78
+ project = self.project or os.environ.get('VERTEXAI_PROJECT', None)
79
+ if not project:
80
+ raise ValueError(
81
+ 'Please specify `project` during `__init__` or set environment '
82
+ 'variable `VERTEXAI_PROJECT` with your Vertex AI project ID.'
83
+ )
84
+
85
+ location = self.location or os.environ.get('VERTEXAI_LOCATION', None)
86
+ if not location:
87
+ raise ValueError(
88
+ 'Please specify `location` during `__init__` or set environment '
89
+ 'variable `VERTEXAI_LOCATION` with your Vertex AI service location.'
90
+ )
91
+
92
+ self._project = project
93
+ self._location = location
94
+
95
+ credentials = self.credentials
96
+ if credentials is None:
97
+ # Use default credentials.
98
+ credentials = google_auth.default(
99
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
100
+ )
101
+ self._credentials = credentials
102
+
103
+ @property
104
+ def model_id(self) -> str:
105
+ """Returns a string to identify the model."""
106
+ return f'VertexAI({self.model})'
107
+
108
+ @functools.cached_property
109
+ def _session(self):
110
+ assert self._api_initialized
111
+ assert self._credentials is not None
112
+ assert auth_requests is not None
113
+ s = auth_requests.AuthorizedSession(self._credentials)
114
+ s.headers.update(self.headers or {})
115
+ return s
116
+
117
+ @property
118
+ def api_endpoint(self) -> str:
119
+ assert self._api_initialized
120
+ return (
121
+ f'https://{self._location}-aiplatform.googleapis.com/v1/projects/'
122
+ f'{self._project}/locations/{self._location}/publishers/google/'
123
+ f'models/{self.model}:generateContent'
124
+ )
125
+
126
+
127
+ class VertexAIGeminiFlash2_0ThinkingExp_20241219(VertexAI): # pylint: disable=invalid-name
128
+ """Vertex AI Gemini Flash 2.0 Thinking model launched on 12/19/2024."""
129
+
130
+ api_version = 'v1alpha'
131
+ model = 'gemini-2.0-flash-thinking-exp-1219'
132
+ timeout = None
133
+
134
+
135
+ class VertexAIGeminiFlash2_0Exp(VertexAI): # pylint: disable=invalid-name
136
+ """Vertex AI Gemini 2.0 Flash model."""
137
+
138
+ model = 'gemini-2.0-flash-exp'
139
+
140
+
141
+ class VertexAIGeminiExp_20241206(VertexAI): # pylint: disable=invalid-name
142
+ """Vertex AI Gemini Experimental model launched on 12/06/2024."""
143
+
144
+ model = 'gemini-exp-1206'
145
+
146
+
147
+ class VertexAIGeminiExp_20241114(VertexAI): # pylint: disable=invalid-name
148
+ """Vertex AI Gemini Experimental model launched on 11/14/2024."""
149
+
150
+ model = 'gemini-exp-1114'
151
+
152
+
153
+ class VertexAIGeminiPro1_5(VertexAI): # pylint: disable=invalid-name
154
+ """Vertex AI Gemini 1.5 Pro model."""
155
+
156
+ model = 'gemini-1.5-pro-latest'
157
+
158
+
159
+ class VertexAIGeminiPro1_5_002(VertexAI): # pylint: disable=invalid-name
160
+ """Vertex AI Gemini 1.5 Pro model."""
161
+
162
+ model = 'gemini-1.5-pro-002'
163
+
164
+
165
+ class VertexAIGeminiPro1_5_001(VertexAI): # pylint: disable=invalid-name
166
+ """Vertex AI Gemini 1.5 Pro model."""
167
+
168
+ model = 'gemini-1.5-pro-001'
169
+
170
+
171
+ class VertexAIGeminiFlash1_5(VertexAI): # pylint: disable=invalid-name
172
+ """Vertex AI Gemini 1.5 Flash model."""
173
+
174
+ model = 'gemini-1.5-flash'
175
+
176
+
177
+ class VertexAIGeminiFlash1_5_002(VertexAI): # pylint: disable=invalid-name
178
+ """Vertex AI Gemini 1.5 Flash model."""
179
+
180
+ model = 'gemini-1.5-flash-002'
181
+
182
+
183
+ class VertexAIGeminiFlash1_5_001(VertexAI): # pylint: disable=invalid-name
184
+ """Vertex AI Gemini 1.5 Flash model."""
185
+
186
+ model = 'gemini-1.5-flash-001'
187
+
188
+
189
+ class VertexAIGeminiPro1(VertexAI): # pylint: disable=invalid-name
190
+ """Vertex AI Gemini 1.0 Pro model."""
191
+
192
+ model = 'gemini-1.0-pro'
@@ -0,0 +1,52 @@
1
+ # Copyright 2024 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Tests for VertexAI models."""
15
+
16
+ import os
17
+ import unittest
18
+ from unittest import mock
19
+
20
+ from langfun.core.llms import vertexai
21
+
22
+
23
+ class VertexAITest(unittest.TestCase):
24
+ """Tests for Vertex model with REST API."""
25
+
26
+ @mock.patch.object(vertexai.VertexAI, 'credentials', new=True)
27
+ def test_project_and_location_check(self):
28
+ with self.assertRaisesRegex(ValueError, 'Please specify `project`'):
29
+ _ = vertexai.VertexAIGeminiPro1()._api_initialized
30
+
31
+ with self.assertRaisesRegex(ValueError, 'Please specify `location`'):
32
+ _ = vertexai.VertexAIGeminiPro1(project='abc')._api_initialized
33
+
34
+ self.assertTrue(
35
+ vertexai.VertexAIGeminiPro1(
36
+ project='abc', location='us-central1'
37
+ )._api_initialized
38
+ )
39
+
40
+ os.environ['VERTEXAI_PROJECT'] = 'abc'
41
+ os.environ['VERTEXAI_LOCATION'] = 'us-central1'
42
+ model = vertexai.VertexAIGeminiPro1()
43
+ self.assertTrue(model.model_id.startswith('VertexAI('))
44
+ self.assertIn('us-central1', model.api_endpoint)
45
+ self.assertTrue(model._api_initialized)
46
+ self.assertIsNotNone(model._session)
47
+ del os.environ['VERTEXAI_PROJECT']
48
+ del os.environ['VERTEXAI_LOCATION']
49
+
50
+
51
+ if __name__ == '__main__':
52
+ unittest.main()
@@ -0,0 +1,284 @@
1
+ # Copyright 2024 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Langfun event logging."""
15
+
16
+ import contextlib
17
+ import datetime
18
+ import functools
19
+ import typing
20
+ from typing import Any, Iterator, Literal
21
+
22
+ from langfun.core import component
23
+ from langfun.core import console
24
+ import pyglove as pg
25
+
26
+
27
+ LogLevel = Literal['debug', 'info', 'error', 'warning', 'fatal']
28
+ _LOG_LEVELS = list(typing.get_args(LogLevel))
29
+
30
+
31
+ @contextlib.contextmanager
32
+ def use_log_level(log_level: LogLevel = 'info') -> Iterator[None]:
33
+ """Contextmanager to enable logging at a given level."""
34
+ with component.context(__event_log_level__=log_level):
35
+ try:
36
+ yield
37
+ finally:
38
+ pass
39
+
40
+
41
+ def get_log_level() -> LogLevel:
42
+ """Gets the current minimum log level."""
43
+ return component.context_value('__event_log_level__', 'info')
44
+
45
+
46
+ class LogEntry(pg.Object, pg.views.HtmlTreeView.Extension):
47
+ """Event log entry."""
48
+ time: datetime.datetime
49
+ level: LogLevel
50
+ message: str
51
+ metadata: dict[str, Any] = pg.Dict()
52
+ indent: int = 0
53
+
54
+ def should_output(self, min_log_level: LogLevel) -> bool:
55
+ return _LOG_LEVELS.index(self.level) >= _LOG_LEVELS.index(min_log_level)
56
+
57
+ def format(self,
58
+ compact: bool = False,
59
+ verbose: bool = True,
60
+ root_indent: int = 0,
61
+ *,
62
+ text_format: bool = True,
63
+ **kwargs):
64
+ if text_format:
65
+ s = f"""{self.time.strftime('%H:%M:%S')} {self.level.upper()} - {self.message}"""
66
+ if self.metadata:
67
+ s += f' (metadata: {self.metadata!r})'
68
+ return s
69
+ return super().format(
70
+ compact=compact,
71
+ verbose=verbose,
72
+ root_indent=root_indent,
73
+ **kwargs
74
+ )
75
+
76
+ def _html_tree_view_summary(
77
+ self,
78
+ view: pg.views.HtmlTreeView,
79
+ title: str | pg.Html | None = None,
80
+ max_summary_len_for_str: int = 80,
81
+ **kwargs
82
+ ) -> pg.Html | None:
83
+ if len(self.message) > max_summary_len_for_str:
84
+ message = self.message[:max_summary_len_for_str] + '...'
85
+ else:
86
+ message = self.message
87
+
88
+ s = pg.Html(
89
+ pg.Html.element(
90
+ 'span',
91
+ [self.time.strftime('%H:%M:%S')],
92
+ css_classes=['log-time']
93
+ ),
94
+ pg.Html.element(
95
+ 'span',
96
+ [pg.Html.escape(message)],
97
+ css_classes=['log-summary'],
98
+ ),
99
+ )
100
+ return view.summary(
101
+ self,
102
+ title=title or s,
103
+ max_summary_len_for_str=max_summary_len_for_str,
104
+ **kwargs,
105
+ )
106
+
107
+ # pytype: disable=annotation-type-mismatch
108
+ def _html_tree_view_content(
109
+ self,
110
+ view: pg.views.HtmlTreeView,
111
+ root_path: pg.KeyPath | None = None,
112
+ max_summary_len_for_str: int = 80,
113
+ collapse_level: int | None = 1,
114
+ extra_flags: dict[str, Any] | None = None,
115
+ **kwargs
116
+ ) -> pg.Html:
117
+ # pytype: enable=annotation-type-mismatch
118
+ extra_flags = extra_flags if extra_flags is not None else {}
119
+ collapse_log_metadata_level: int | None = extra_flags.get(
120
+ 'collapse_log_metadata_level', None
121
+ )
122
+ def render_message_text():
123
+ if len(self.message) < max_summary_len_for_str:
124
+ return None
125
+ return pg.Html.element(
126
+ 'span',
127
+ [pg.Html.escape(self.message)],
128
+ css_classes=['log-text'],
129
+ )
130
+
131
+ def render_metadata():
132
+ if not self.metadata:
133
+ return None
134
+ return pg.Html.element(
135
+ 'div',
136
+ [
137
+ view.render(
138
+ self.metadata,
139
+ name='metadata',
140
+ root_path=pg.KeyPath('metadata', root_path),
141
+ parent=self,
142
+ collapse_level=view.get_collapse_level(
143
+ (collapse_level, -1), collapse_log_metadata_level,
144
+ ),
145
+ max_summary_len_for_str=max_summary_len_for_str,
146
+ extra_flags=extra_flags,
147
+ **view.get_passthrough_kwargs(**kwargs),
148
+ )
149
+ ],
150
+ css_classes=['log-metadata'],
151
+ )
152
+
153
+ return pg.Html.element(
154
+ 'div',
155
+ [
156
+ render_message_text(),
157
+ render_metadata(),
158
+ ],
159
+ css_classes=['complex_value'],
160
+ )
161
+
162
+ def _html_tree_view_config(self) -> dict[str, Any]:
163
+ return pg.views.HtmlTreeView.get_kwargs(
164
+ super()._html_tree_view_config(),
165
+ dict(
166
+ css_classes=[f'log-{self.level}'],
167
+ )
168
+ )
169
+
170
+ @classmethod
171
+ @functools.cache
172
+ def _html_tree_view_css_styles(cls) -> list[str]:
173
+ return super()._html_tree_view_css_styles() + [
174
+ """
175
+ /* Langfun LogEntry styles. */
176
+ .log-time {
177
+ color: #222;
178
+ font-size: 12px;
179
+ padding-right: 10px;
180
+ }
181
+ .log-summary {
182
+ font-weight: normal;
183
+ font-style: italic;
184
+ padding: 4px;
185
+ }
186
+ .log-debug > summary > .summary_title::before {
187
+ content: '🛠️ '
188
+ }
189
+ .log-info > summary > .summary_title::before {
190
+ content: '💡 '
191
+ }
192
+ .log-warning > summary > .summary_title::before {
193
+ content: '❗ '
194
+ }
195
+ .log-error > summary > .summary_title::before {
196
+ content: '❌ '
197
+ }
198
+ .log-fatal > summary > .summary_title::before {
199
+ content: '💀 '
200
+ }
201
+ .log-text {
202
+ display: block;
203
+ color: black;
204
+ font-style: italic;
205
+ padding: 20px;
206
+ border-radius: 5px;
207
+ background: rgba(255, 255, 255, 0.5);
208
+ white-space: pre-wrap;
209
+ }
210
+ details.log-entry {
211
+ margin: 0px 0px 10px;
212
+ border: 0px;
213
+ }
214
+ div.log-metadata {
215
+ margin: 10px 0px 0px 0px;
216
+ }
217
+ .log-metadata > details {
218
+ background-color: rgba(255, 255, 255, 0.5);
219
+ border: 1px solid transparent;
220
+ }
221
+ .log-debug {
222
+ background-color: #EEEEEE
223
+ }
224
+ .log-warning {
225
+ background-color: #F8C471
226
+ }
227
+ .log-info {
228
+ background-color: #A3E4D7
229
+ }
230
+ .log-error {
231
+ background-color: #F5C6CB
232
+ }
233
+ .log-fatal {
234
+ background-color: #F19CBB
235
+ }
236
+ """
237
+ ]
238
+
239
+
240
+ def log(level: LogLevel,
241
+ message: str,
242
+ *,
243
+ indent: int = 0,
244
+ **kwargs) -> LogEntry:
245
+ """Logs a message."""
246
+ entry = LogEntry(
247
+ indent=indent,
248
+ level=level,
249
+ time=datetime.datetime.now(),
250
+ message=message,
251
+ metadata=kwargs,
252
+ )
253
+ if entry.should_output(get_log_level()):
254
+ if console.under_notebook():
255
+ console.display(entry)
256
+ else:
257
+ # TODO(daiyip): Improve the console output formatting.
258
+ console.write(entry)
259
+ return entry
260
+
261
+
262
+ def debug(message: str, *, indent: int = 0, **kwargs) -> LogEntry:
263
+ """Logs a debug message to the session."""
264
+ return log('debug', message, indent=indent, **kwargs)
265
+
266
+
267
+ def info(message: str, *, indent: int = 0, **kwargs) -> LogEntry:
268
+ """Logs an info message to the session."""
269
+ return log('info', message, indent=indent, **kwargs)
270
+
271
+
272
+ def warning(message: str, *, indent: int = 0, **kwargs) -> LogEntry:
273
+ """Logs an info message to the session."""
274
+ return log('warning', message, indent=indent, **kwargs)
275
+
276
+
277
+ def error(message: str, *, indent: int = 0, **kwargs) -> LogEntry:
278
+ """Logs an error message to the session."""
279
+ return log('error', message, indent=indent, **kwargs)
280
+
281
+
282
+ def fatal(message: str, *, indent: int = 0, **kwargs) -> LogEntry:
283
+ """Logs a fatal message to the session."""
284
+ return log('fatal', message, indent=indent, **kwargs)
@@ -0,0 +1,125 @@
1
+ # Copyright 2024 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Tests for langfun.core.logging."""
15
+
16
+ import datetime
17
+ import inspect
18
+ import unittest
19
+
20
+ from langfun.core import logging
21
+
22
+
23
+ class LoggingTest(unittest.TestCase):
24
+
25
+ def test_use_log_level(self):
26
+ self.assertEqual(logging.get_log_level(), 'info')
27
+ with logging.use_log_level('debug'):
28
+ self.assertEqual(logging.get_log_level(), 'debug')
29
+ with logging.use_log_level(None):
30
+ self.assertIsNone(logging.get_log_level(), None)
31
+ self.assertEqual(logging.get_log_level(), 'debug')
32
+ self.assertEqual(logging.get_log_level(), 'info')
33
+
34
+ def test_log(self):
35
+ entry = logging.log('info', 'hi', indent=1, x=1, y=2)
36
+ self.assertEqual(entry.level, 'info')
37
+ self.assertEqual(entry.message, 'hi')
38
+ self.assertEqual(entry.indent, 1)
39
+ self.assertEqual(entry.metadata, {'x': 1, 'y': 2})
40
+
41
+ self.assertEqual(logging.debug('hi').level, 'debug')
42
+ self.assertEqual(logging.info('hi').level, 'info')
43
+ self.assertEqual(logging.warning('hi').level, 'warning')
44
+ self.assertEqual(logging.error('hi').level, 'error')
45
+ self.assertEqual(logging.fatal('hi').level, 'fatal')
46
+
47
+ def test_repr_html(self):
48
+ def assert_color(entry, color):
49
+ self.assertIn(f'background-color: {color}', entry._repr_html_())
50
+
51
+ assert_color(logging.debug('hi', indent=0), '#EEEEEE')
52
+ assert_color(logging.info('hi', indent=1), '#A3E4D7')
53
+ assert_color(logging.warning('hi', x=1, y=2), '#F8C471')
54
+ assert_color(logging.error('hi', indent=2, x=1, y=2), '#F5C6CB')
55
+ assert_color(logging.fatal('hi', indent=2, x=1, y=2), '#F19CBB')
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_format(self):
65
+ time = datetime.datetime(2024, 10, 10, 12, 30, 45)
66
+ self.assertEqual(
67
+ str(
68
+ logging.LogEntry(
69
+ level='info', message='hello\nworld',
70
+ time=time, metadata=dict(x=1),
71
+ )
72
+ ),
73
+ '12:30:45 INFO - hello\nworld (metadata: {x=1})',
74
+ )
75
+ self.assertIn(
76
+ 'LogEntry(',
77
+ logging.LogEntry(
78
+ level='info', message='hello\nworld',
79
+ time=time, metadata=dict(x=1),
80
+ ).format(text_format=False),
81
+ )
82
+
83
+ def test_html(self):
84
+ time = datetime.datetime(2024, 10, 10, 12, 30, 45)
85
+ self.assert_html_content(
86
+ logging.LogEntry(
87
+ level='info', message='5 + 2 > 3',
88
+ time=time, metadata={}
89
+ ).to_html(enable_summary_tooltip=False),
90
+ """
91
+ <details open class="pyglove log-entry log-info"><summary><div class="summary-title log-info"><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>
92
+ """
93
+ )
94
+ self.assert_html_content(
95
+ logging.LogEntry(
96
+ level='error', message='This is a longer message: 5 + 2 > 3',
97
+ time=time, metadata=dict(x=dict(z=1), y=2)
98
+ ).to_html(
99
+ extra_flags=dict(
100
+ collapse_log_metadata_level=1,
101
+ ),
102
+ max_summary_len_for_str=10,
103
+ enable_summary_tooltip=False,
104
+ ),
105
+ """
106
+ <details open class="pyglove log-entry log-error"><summary><div class="summary-title log-error"><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<span class="tooltip">metadata</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details class="pyglove dict"><summary><div class="summary-name">x<span class="tooltip">metadata.x</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details open class="pyglove int"><summary><div class="summary-name">z<span class="tooltip">metadata.x.z</span></div><div class="summary-title">int</div></summary><span class="simple-value int">1</span></details></div></details><details open class="pyglove int"><summary><div class="summary-name">y<span class="tooltip">metadata.y</span></div><div class="summary-title">int</div></summary><span class="simple-value int">2</span></details></div></details></div></div></details>
107
+ """
108
+ )
109
+ self.assert_html_content(
110
+ logging.LogEntry(
111
+ level='error', message='This is a longer message: 5 + 2 > 3',
112
+ time=time, metadata=dict(x=dict(z=1), y=2)
113
+ ).to_html(
114
+ extra_flags=dict(
115
+ max_summary_len_for_str=10,
116
+ ),
117
+ enable_summary_tooltip=False,
118
+ ),
119
+ """
120
+ <details open class="pyglove log-entry log-error"><summary><div class="summary-title log-error"><span class="log-time">12:30:45</span><span class="log-summary">This is a longer message: 5 + 2 &gt; 3</span></div></summary><div class="complex_value"><div class="log-metadata"><details open class="pyglove dict"><summary><div class="summary-name">metadata<span class="tooltip">metadata</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details open class="pyglove dict"><summary><div class="summary-name">x<span class="tooltip">metadata.x</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details open class="pyglove int"><summary><div class="summary-name">z<span class="tooltip">metadata.x.z</span></div><div class="summary-title">int</div></summary><span class="simple-value int">1</span></details></div></details><details open class="pyglove int"><summary><div class="summary-name">y<span class="tooltip">metadata.y</span></div><div class="summary-title">int</div></summary><span class="simple-value int">2</span></details></div></details></div></div></details>
121
+ """
122
+ )
123
+
124
+ if __name__ == '__main__':
125
+ unittest.main()