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.
- langfun/__init__.py +20 -2
- langfun/core/__init__.py +16 -5
- langfun/core/agentic/__init__.py +30 -0
- langfun/core/agentic/action.py +854 -0
- langfun/core/agentic/action_eval.py +150 -0
- langfun/core/agentic/action_eval_test.py +109 -0
- langfun/core/agentic/action_test.py +136 -0
- langfun/core/coding/python/__init__.py +5 -11
- langfun/core/coding/python/correction.py +37 -21
- langfun/core/coding/python/correction_test.py +29 -3
- langfun/core/coding/python/execution.py +40 -216
- langfun/core/coding/python/execution_test.py +29 -89
- langfun/core/coding/python/generation.py +21 -11
- langfun/core/coding/python/generation_test.py +2 -2
- langfun/core/coding/python/parsing.py +108 -193
- langfun/core/coding/python/parsing_test.py +2 -105
- langfun/core/component.py +63 -2
- langfun/core/component_test.py +53 -0
- langfun/core/concurrent.py +414 -117
- langfun/core/concurrent_test.py +111 -24
- langfun/core/console.py +18 -5
- langfun/core/console_test.py +17 -0
- langfun/core/eval/__init__.py +16 -1
- langfun/core/eval/base.py +622 -174
- langfun/core/eval/base_test.py +200 -54
- langfun/core/eval/matching.py +63 -76
- langfun/core/eval/matching_test.py +17 -8
- langfun/core/eval/patching.py +130 -0
- langfun/core/eval/patching_test.py +170 -0
- langfun/core/eval/scoring.py +26 -26
- langfun/core/eval/scoring_test.py +19 -2
- langfun/core/eval/v2/__init__.py +42 -0
- langfun/core/eval/v2/checkpointing.py +380 -0
- langfun/core/eval/v2/checkpointing_test.py +228 -0
- langfun/core/eval/v2/eval_test_helper.py +136 -0
- langfun/core/eval/v2/evaluation.py +725 -0
- langfun/core/eval/v2/evaluation_test.py +180 -0
- langfun/core/eval/v2/example.py +305 -0
- langfun/core/eval/v2/example_test.py +128 -0
- langfun/core/eval/v2/experiment.py +1048 -0
- langfun/core/eval/v2/experiment_test.py +433 -0
- langfun/core/eval/v2/metric_values.py +156 -0
- langfun/core/eval/v2/metric_values_test.py +80 -0
- langfun/core/eval/v2/metrics.py +357 -0
- langfun/core/eval/v2/metrics_test.py +203 -0
- langfun/core/eval/v2/progress.py +348 -0
- langfun/core/eval/v2/progress_test.py +82 -0
- langfun/core/eval/v2/progress_tracking.py +210 -0
- langfun/core/eval/v2/progress_tracking_test.py +66 -0
- langfun/core/eval/v2/reporting.py +270 -0
- langfun/core/eval/v2/reporting_test.py +158 -0
- langfun/core/eval/v2/runners.py +488 -0
- langfun/core/eval/v2/runners_test.py +334 -0
- langfun/core/langfunc.py +4 -17
- langfun/core/langfunc_test.py +22 -6
- langfun/core/language_model.py +577 -39
- langfun/core/language_model_test.py +470 -56
- langfun/core/llms/__init__.py +87 -16
- langfun/core/llms/anthropic.py +312 -87
- langfun/core/llms/anthropic_test.py +71 -3
- langfun/core/llms/cache/base.py +21 -2
- langfun/core/llms/cache/in_memory.py +13 -0
- langfun/core/llms/cache/in_memory_test.py +53 -2
- langfun/core/llms/compositional.py +101 -0
- langfun/core/llms/compositional_test.py +73 -0
- langfun/core/llms/deepseek.py +117 -0
- langfun/core/llms/deepseek_test.py +61 -0
- langfun/core/llms/fake.py +11 -7
- langfun/core/llms/fake_test.py +14 -0
- langfun/core/llms/gemini.py +507 -0
- langfun/core/llms/gemini_test.py +195 -0
- langfun/core/llms/google_genai.py +62 -218
- langfun/core/llms/google_genai_test.py +9 -202
- langfun/core/llms/groq.py +160 -144
- langfun/core/llms/groq_test.py +31 -137
- langfun/core/llms/llama_cpp.py +15 -42
- langfun/core/llms/llama_cpp_test.py +4 -30
- langfun/core/llms/openai.py +395 -203
- langfun/core/llms/openai_compatible.py +179 -0
- langfun/core/llms/openai_compatible_test.py +495 -0
- langfun/core/llms/openai_test.py +30 -395
- langfun/core/llms/rest.py +113 -0
- langfun/core/llms/rest_test.py +111 -0
- langfun/core/llms/vertexai.py +192 -0
- langfun/core/llms/vertexai_test.py +52 -0
- langfun/core/logging.py +284 -0
- langfun/core/logging_test.py +125 -0
- langfun/core/message.py +319 -9
- langfun/core/message_test.py +190 -13
- langfun/core/modalities/__init__.py +6 -2
- langfun/core/modalities/audio.py +30 -0
- langfun/core/modalities/audio_test.py +63 -0
- langfun/core/modalities/image.py +39 -20
- langfun/core/modalities/image_test.py +52 -9
- langfun/core/modalities/mime.py +206 -29
- langfun/core/modalities/mime_test.py +90 -9
- langfun/core/modalities/ms_office.py +117 -0
- langfun/core/modalities/ms_office_test.py +389 -0
- langfun/core/modalities/pdf.py +22 -0
- langfun/core/modalities/pdf_test.py +57 -0
- langfun/core/modalities/video.py +9 -26
- langfun/core/modalities/video_test.py +3 -3
- langfun/core/modality.py +26 -3
- langfun/core/modality_test.py +2 -2
- langfun/core/sampling.py +11 -11
- langfun/core/structured/__init__.py +12 -16
- langfun/core/structured/completion.py +32 -5
- langfun/core/structured/completion_test.py +7 -6
- langfun/core/structured/description.py +2 -2
- langfun/core/structured/description_test.py +3 -3
- langfun/core/structured/function_generation.py +60 -27
- langfun/core/structured/function_generation_test.py +72 -2
- langfun/core/structured/mapping.py +97 -47
- langfun/core/structured/mapping_test.py +90 -2
- langfun/core/structured/parsing.py +33 -21
- langfun/core/structured/parsing_test.py +53 -9
- langfun/core/structured/querying.py +746 -0
- langfun/core/structured/{prompting_test.py → querying_test.py} +469 -51
- langfun/core/structured/schema.py +204 -97
- langfun/core/structured/schema_generation.py +1 -1
- langfun/core/structured/schema_test.py +130 -29
- langfun/core/structured/scoring.py +125 -19
- langfun/core/structured/scoring_test.py +30 -0
- langfun/core/structured/tokenization.py +64 -0
- langfun/core/structured/tokenization_test.py +48 -0
- langfun/core/template.py +115 -1
- langfun/core/template_test.py +71 -1
- langfun/core/templates/conversation.py +9 -0
- langfun/core/templates/conversation_test.py +4 -3
- langfun/core/templates/selfplay_test.py +10 -2
- langfun-0.1.2.dev202501140804.dist-info/METADATA +225 -0
- langfun-0.1.2.dev202501140804.dist-info/RECORD +153 -0
- {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/WHEEL +1 -1
- langfun/core/coding/python/errors.py +0 -108
- langfun/core/coding/python/errors_test.py +0 -99
- langfun/core/coding/python/permissions.py +0 -90
- langfun/core/coding/python/permissions_test.py +0 -86
- langfun/core/structured/prompting.py +0 -238
- langfun/core/text_formatting.py +0 -162
- langfun/core/text_formatting_test.py +0 -47
- langfun-0.0.2.dev20240429.dist-info/METADATA +0 -100
- langfun-0.0.2.dev20240429.dist-info/RECORD +0 -108
- {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/LICENSE +0 -0
- {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()
|
langfun/core/logging.py
ADDED
@@ -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 > 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 > 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 > 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()
|