langfun 0.0.2.dev20240330__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 (145) hide show
  1. langfun/__init__.py +22 -2
  2. langfun/core/__init__.py +17 -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 -28
  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 +69 -2
  18. langfun/core/component_test.py +54 -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 +17 -0
  24. langfun/core/eval/base.py +767 -140
  25. langfun/core/eval/base_test.py +238 -53
  26. langfun/core/eval/matching.py +80 -76
  27. langfun/core/eval/matching_test.py +19 -9
  28. langfun/core/eval/patching.py +130 -0
  29. langfun/core/eval/patching_test.py +170 -0
  30. langfun/core/eval/scoring.py +37 -28
  31. langfun/core/eval/scoring_test.py +21 -3
  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 +3 -21
  55. langfun/core/langfunc_test.py +26 -8
  56. langfun/core/language_model.py +686 -48
  57. langfun/core/language_model_test.py +681 -44
  58. langfun/core/llms/__init__.py +100 -12
  59. langfun/core/llms/anthropic.py +488 -0
  60. langfun/core/llms/anthropic_test.py +235 -0
  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 +88 -28
  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 +39 -26
  69. langfun/core/llms/fake_test.py +136 -11
  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 -197
  74. langfun/core/llms/groq.py +276 -0
  75. langfun/core/llms/groq_test.py +64 -0
  76. langfun/core/llms/llama_cpp.py +15 -40
  77. langfun/core/llms/llama_cpp_test.py +4 -30
  78. langfun/core/llms/openai.py +436 -226
  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 +35 -174
  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 -23
  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 +15 -16
  107. langfun/core/structured/completion.py +32 -5
  108. langfun/core/structured/completion_test.py +9 -8
  109. langfun/core/structured/description.py +2 -2
  110. langfun/core/structured/description_test.py +3 -3
  111. langfun/core/structured/function_generation.py +278 -0
  112. langfun/core/structured/function_generation_test.py +399 -0
  113. langfun/core/structured/mapping.py +150 -46
  114. langfun/core/structured/mapping_test.py +105 -0
  115. langfun/core/structured/parsing.py +33 -21
  116. langfun/core/structured/parsing_test.py +71 -22
  117. langfun/core/structured/querying.py +746 -0
  118. langfun/core/structured/{prompting_test.py → querying_test.py} +545 -60
  119. langfun/core/structured/schema.py +208 -99
  120. langfun/core/structured/schema_generation.py +1 -1
  121. langfun/core/structured/schema_generation_test.py +2 -2
  122. langfun/core/structured/schema_test.py +133 -34
  123. langfun/core/structured/scoring.py +125 -19
  124. langfun/core/structured/scoring_test.py +30 -0
  125. langfun/core/structured/tokenization.py +64 -0
  126. langfun/core/structured/tokenization_test.py +48 -0
  127. langfun/core/template.py +240 -11
  128. langfun/core/template_test.py +146 -1
  129. langfun/core/templates/conversation.py +9 -0
  130. langfun/core/templates/conversation_test.py +4 -3
  131. langfun/core/templates/selfplay_test.py +14 -2
  132. langfun-0.1.2.dev202501140804.dist-info/METADATA +225 -0
  133. langfun-0.1.2.dev202501140804.dist-info/RECORD +153 -0
  134. {langfun-0.0.2.dev20240330.dist-info → langfun-0.1.2.dev202501140804.dist-info}/WHEEL +1 -1
  135. langfun/core/coding/python/errors.py +0 -108
  136. langfun/core/coding/python/errors_test.py +0 -99
  137. langfun/core/coding/python/permissions.py +0 -90
  138. langfun/core/coding/python/permissions_test.py +0 -86
  139. langfun/core/structured/prompting.py +0 -217
  140. langfun/core/text_formatting.py +0 -162
  141. langfun/core/text_formatting_test.py +0 -47
  142. langfun-0.0.2.dev20240330.dist-info/METADATA +0 -99
  143. langfun-0.0.2.dev20240330.dist-info/RECORD +0 -102
  144. {langfun-0.0.2.dev20240330.dist-info → langfun-0.1.2.dev202501140804.dist-info}/LICENSE +0 -0
  145. {langfun-0.0.2.dev20240330.dist-info → langfun-0.1.2.dev202501140804.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,270 @@
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
+ """Reporting evaluation results."""
15
+
16
+ import threading
17
+ import time
18
+ import traceback
19
+ from typing import Annotated
20
+
21
+ from langfun.core.eval.v2 import example as example_lib
22
+ from langfun.core.eval.v2 import experiment as experiment_lib
23
+ import pyglove as pg
24
+
25
+ Runner = experiment_lib.Runner
26
+ Experiment = experiment_lib.Experiment
27
+ Example = example_lib.Example
28
+
29
+
30
+ _SUMMARY_FILE = 'summary.html'
31
+ _EVALULATION_DETAIL_FILE = 'index.html'
32
+
33
+
34
+ class HtmlReporter(experiment_lib.Plugin):
35
+ """Plugin for periodically generating HTML reports for the experiment."""
36
+
37
+ summary_interval: Annotated[
38
+ int,
39
+ 'The interval of writing summary in seconds.'
40
+ ] = 60
41
+
42
+ experiment_report_interval: Annotated[
43
+ int,
44
+ 'The interval of writing report for inidividual experiments in seconds.'
45
+ ] = 120
46
+
47
+ def _on_bound(self):
48
+ super()._on_bound()
49
+ self._last_summary_time = 0
50
+ self._last_experiment_report_time = {}
51
+ self._update_thread = None
52
+ self._stop_update = False
53
+ self._stop_update_experiment_ids = set()
54
+ self._summary_lock = None
55
+ self._experiment_index_lock = None
56
+
57
+ def on_run_start(
58
+ self,
59
+ runner: Runner,
60
+ root: Experiment
61
+ ) -> None:
62
+ self._maybe_update_summary(runner)
63
+ self._last_experiment_report_time = {leaf.id: 0 for leaf in root.leaf_nodes}
64
+ self._stop_update = False
65
+ self._stop_update_experiment_ids = set()
66
+ self._summary_lock = threading.Lock()
67
+ self._experiment_index_lock = {
68
+ leaf.id: threading.Lock() for leaf in root.leaf_nodes
69
+ }
70
+ self._update_thread = threading.Thread(
71
+ target=self._update_thread_func, args=(runner,)
72
+ )
73
+ self._update_thread.start()
74
+
75
+ def on_run_complete(
76
+ self,
77
+ runner: Runner,
78
+ root: Experiment
79
+ ) -> None:
80
+ self._stop_update = True
81
+ self._maybe_update_summary(runner, force=True)
82
+
83
+ def on_run_abort(
84
+ self,
85
+ runner: Runner,
86
+ root: Experiment,
87
+ error: BaseException
88
+ ) -> None:
89
+ self._stop_update = True
90
+ self._maybe_update_summary(runner, force=True)
91
+
92
+ def _update_thread_func(self, runner: Runner):
93
+ while not self._stop_update:
94
+ self._maybe_update_summary(runner, background=False)
95
+ for leaf in runner.current_run.experiment.leaf_nodes:
96
+ if leaf.id in self._stop_update_experiment_ids:
97
+ continue
98
+ self._maybe_update_experiment_html(runner, leaf, background=False)
99
+ if leaf.progress.is_stopped:
100
+ self._stop_update_experiment_ids.add(leaf.id)
101
+ time.sleep(5)
102
+
103
+ def on_experiment_start(
104
+ self,
105
+ runner: Runner,
106
+ experiment: Experiment
107
+ ) -> None:
108
+ if experiment.is_leaf:
109
+ self._maybe_update_experiment_html(runner, experiment)
110
+
111
+ def on_experiment_complete(
112
+ self, runner: Runner, experiment: Experiment
113
+ ):
114
+ if experiment.is_leaf:
115
+ self._maybe_update_experiment_html(runner, experiment, force=True)
116
+
117
+ def on_experiment_abort(
118
+ self,
119
+ runner: Runner,
120
+ experiment: Experiment,
121
+ error: BaseException
122
+ ) -> None:
123
+ del error
124
+ assert experiment.is_leaf
125
+ self._maybe_update_experiment_html(runner, experiment, force=True)
126
+
127
+ def on_example_complete(
128
+ self, runner: Runner, experiment: Experiment, example: Example
129
+ ):
130
+ self._save_example_html(runner, experiment, example)
131
+ self._maybe_update_experiment_html(runner, experiment)
132
+ self._maybe_update_summary(runner)
133
+
134
+ def _maybe_update_summary(
135
+ self,
136
+ runner: Runner,
137
+ background: bool = True,
138
+ force: bool = False) -> None:
139
+ """Maybe update the summary of current run."""
140
+ run = runner.current_run
141
+ def _summary():
142
+ html = run.experiment.to_html(
143
+ collapse_level=None,
144
+ extra_flags=dict(
145
+ current_run=run, interactive=False, card_view=True,
146
+ )
147
+ )
148
+ with self._summary_lock:
149
+ html.save(
150
+ run.output_path_for(run.experiment, _SUMMARY_FILE)
151
+ )
152
+
153
+ if force or (time.time() - self._last_summary_time > self.summary_interval):
154
+ self._last_summary_time = time.time()
155
+ if background:
156
+ runner.background_run(_summary)
157
+ else:
158
+ _summary()
159
+
160
+ def _maybe_update_experiment_html(
161
+ self,
162
+ runner: Runner,
163
+ experiment: Experiment,
164
+ force: bool = False,
165
+ background: bool = True,
166
+ ) -> None:
167
+ def _save():
168
+ index_html_path = runner.current_run.output_path_for(
169
+ experiment, _EVALULATION_DETAIL_FILE
170
+ )
171
+ try:
172
+ with pg.timeit() as t:
173
+ html = experiment.to_html(
174
+ collapse_level=None,
175
+ extra_flags=dict(
176
+ current_run=runner.current_run,
177
+ interactive=False,
178
+ card_view=False,
179
+ ),
180
+ )
181
+ with self._experiment_index_lock[experiment.id]:
182
+ html.save(index_html_path)
183
+ experiment.info(
184
+ f'Updated {index_html_path!r} in {t.elapse:.2f} seconds.',
185
+ )
186
+ except BaseException as e: # pylint: disable=broad-except
187
+ experiment.error(
188
+ f'Failed to generate {index_html_path!r}. '
189
+ f'Error: {e}, Stacktrace: \n{traceback.format_exc()}.',
190
+ )
191
+ raise e
192
+
193
+ if force or (
194
+ time.time() - self._last_experiment_report_time[experiment.id]
195
+ > self.experiment_report_interval
196
+ ):
197
+ self._last_experiment_report_time[experiment.id] = time.time()
198
+ if background:
199
+ runner.background_run(_save)
200
+ else:
201
+ _save()
202
+
203
+ def _save_example_html(
204
+ self, runner: Runner, experiment: Experiment, example: Example
205
+ ) -> None:
206
+ """Saves the example in HTML format."""
207
+ current_run = runner.current_run
208
+ def _generate():
209
+ try:
210
+ with pg.timeit() as t:
211
+ html = example.to_html(
212
+ collapse_level=None,
213
+ enable_summary_tooltip=False,
214
+ extra_flags=dict(
215
+ # For properly rendering the next link.
216
+ num_examples=getattr(experiment, 'num_examples', None)
217
+ ),
218
+ )
219
+ html.save(
220
+ runner.current_run.output_path_for(
221
+ experiment, f'{example.id}.html'
222
+ )
223
+ )
224
+ experiment.info(
225
+ f'\'{example.id}.html\' generated in {t.elapse:.2f} seconds. '
226
+ )
227
+ except BaseException as e: # pylint: disable=broad-except
228
+ experiment.error(
229
+ f'Failed to generate \'{example.id}.html\'. '
230
+ f'Error: {e}, Stacktrace: \n{traceback.format_exc()}.',
231
+ )
232
+ raise e
233
+
234
+ def _copy():
235
+ src_file = current_run.input_path_for(experiment, f'{example.id}.html')
236
+ dest_file = current_run.output_path_for(experiment, f'{example.id}.html')
237
+
238
+ if src_file == dest_file:
239
+ return
240
+
241
+ if not pg.io.path_exists(src_file):
242
+ experiment.warning(
243
+ f'Skip copying \'{example.id}.html\' as '
244
+ f'{src_file!r} does not exist.'
245
+ )
246
+ return
247
+
248
+ try:
249
+ with pg.timeit() as t, pg.io.open(src_file, 'r') as src:
250
+ content = src.read()
251
+ with pg.io.open(dest_file, 'w') as dest:
252
+ dest.write(content)
253
+ experiment.info(
254
+ f'\'{example.id}.html\' copied in {t.elapse:.2f} seconds.'
255
+ )
256
+ except BaseException as e: # pylint: disable=broad-except
257
+ experiment.error(
258
+ f'Failed to copy {src_file!r} to {dest_file!r}. Error: {e}.'
259
+ )
260
+ raise e
261
+
262
+ generate_example_html = current_run.generate_example_html
263
+ if (generate_example_html == 'all'
264
+ or (generate_example_html == 'new' and example.newly_processed)
265
+ or (isinstance(generate_example_html, list)
266
+ and example.id in generate_example_html)):
267
+ op = _generate
268
+ else:
269
+ op = _copy
270
+ runner.background_run(op)
@@ -0,0 +1,158 @@
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
+ import os
15
+ import tempfile
16
+ import unittest
17
+
18
+ from langfun.core.eval.v2 import checkpointing
19
+ from langfun.core.eval.v2 import eval_test_helper
20
+ from langfun.core.eval.v2 import reporting
21
+ from langfun.core.eval.v2 import runners as runners_lib # pylint: disable=unused-import
22
+ import pyglove as pg
23
+
24
+
25
+ class ReportingTest(unittest.TestCase):
26
+
27
+ def test_reporting(self):
28
+ root_dir = os.path.join(tempfile.gettempdir(), 'test_reporting')
29
+ experiment = eval_test_helper.test_experiment()
30
+ checkpointer = checkpointing.BulkCheckpointer('checkpoint.jsonl')
31
+ reporter = reporting.HtmlReporter()
32
+ run = experiment.run(root_dir, 'new', plugins=[checkpointer, reporter])
33
+ self.assertTrue(
34
+ pg.io.path_exists(run.output_path_for(experiment, 'summary.html'))
35
+ )
36
+ for leaf in experiment.leaf_nodes:
37
+ self.assertTrue(
38
+ pg.io.path_exists(run.output_path_for(leaf, 'index.html'))
39
+ )
40
+ for i in range(leaf.num_examples):
41
+ self.assertTrue(
42
+ pg.io.path_exists(run.output_path_for(leaf, f'{i + 1}.html'))
43
+ )
44
+ found_generation_log = False
45
+ for log_entry in leaf._log_entries:
46
+ if 'generated in' in log_entry.message:
47
+ found_generation_log = True
48
+ break
49
+ self.assertTrue(found_generation_log)
50
+
51
+ # Test warm start.
52
+ root_dir = os.path.join(tempfile.gettempdir(), 'test_reporting2')
53
+ experiment = eval_test_helper.test_experiment()
54
+ run = experiment.run(
55
+ root_dir, 'new', plugins=[checkpointer, reporter],
56
+ warm_start_from=run.output_root
57
+ )
58
+ self.assertTrue(
59
+ pg.io.path_exists(run.output_path_for(experiment, 'summary.html'))
60
+ )
61
+ for leaf in experiment.leaf_nodes:
62
+ self.assertTrue(
63
+ pg.io.path_exists(run.output_path_for(leaf, 'index.html'))
64
+ )
65
+ for i in range(leaf.num_examples):
66
+ self.assertTrue(
67
+ pg.io.path_exists(run.output_path_for(leaf, f'{i + 1}.html'))
68
+ )
69
+ found_copy_log = False
70
+ for log_entry in leaf._log_entries:
71
+ if 'copied in' in log_entry.message:
72
+ found_copy_log = True
73
+ break
74
+ self.assertTrue(found_copy_log)
75
+
76
+ def test_index_html_generation_error(self):
77
+ root_dir = os.path.join(
78
+ tempfile.gettempdir(),
79
+ 'test_reporting_with_index_html_generation_error'
80
+ )
81
+ experiment = (eval_test_helper
82
+ .test_experiment_with_index_html_generation_error())
83
+ reporter = reporting.HtmlReporter()
84
+ run = experiment.run(root_dir, 'new', plugins=[reporter])
85
+ self.assertFalse(
86
+ pg.io.path_exists(run.output_path_for(experiment, 'summary.html'))
87
+ )
88
+ for leaf in experiment.leaf_nodes:
89
+ self.assertFalse(
90
+ pg.io.path_exists(run.output_path_for(leaf, 'index.html'))
91
+ )
92
+ found_error_log = False
93
+ for log_entry in experiment._log_entries:
94
+ if log_entry.message.startswith('Failed to generate'):
95
+ found_error_log = True
96
+ break
97
+ self.assertTrue(found_error_log)
98
+
99
+ def test_example_html_generation_error(self):
100
+ root_dir = os.path.join(
101
+ tempfile.gettempdir(),
102
+ 'test_reporting_with_example_html_generation_error'
103
+ )
104
+ experiment = (eval_test_helper
105
+ .test_experiment_with_example_html_generation_error())
106
+ checkpointer = checkpointing.BulkCheckpointer('checkpoint.jsonl')
107
+ reporter = reporting.HtmlReporter()
108
+ run = experiment.run(root_dir, 'new', plugins=[checkpointer, reporter])
109
+ self.assertTrue(
110
+ pg.io.path_exists(run.output_path_for(experiment, 'summary.html'))
111
+ )
112
+ for leaf in experiment.leaf_nodes:
113
+ self.assertTrue(
114
+ pg.io.path_exists(run.output_path_for(leaf, 'index.html'))
115
+ )
116
+ for i in range(leaf.num_examples):
117
+ self.assertFalse(
118
+ pg.io.path_exists(run.output_path_for(leaf, f'{i + 1}.html'))
119
+ )
120
+ found_error_log = False
121
+ for log_entry in experiment._log_entries:
122
+ if log_entry.message.startswith('Failed to generate'):
123
+ found_error_log = True
124
+ break
125
+ self.assertTrue(found_error_log)
126
+
127
+ # Test warm start.
128
+ root_dir = os.path.join(
129
+ tempfile.gettempdir(),
130
+ 'test_reporting_with_example_html_generation_error2'
131
+ )
132
+ experiment = (eval_test_helper
133
+ .test_experiment_with_example_html_generation_error())
134
+ run = experiment.run(
135
+ root_dir, 'new', plugins=[checkpointer, reporter],
136
+ warm_start_from=run.output_root
137
+ )
138
+ self.assertTrue(
139
+ pg.io.path_exists(run.output_path_for(experiment, 'summary.html'))
140
+ )
141
+ for leaf in experiment.leaf_nodes:
142
+ self.assertTrue(
143
+ pg.io.path_exists(run.output_path_for(leaf, 'index.html'))
144
+ )
145
+ for i in range(leaf.num_examples):
146
+ self.assertFalse(
147
+ pg.io.path_exists(run.output_path_for(leaf, f'{i + 1}.html'))
148
+ )
149
+ found_error_log = False
150
+ for log_entry in experiment._log_entries:
151
+ if log_entry.message.startswith('Skip copying'):
152
+ found_error_log = True
153
+ break
154
+ self.assertTrue(found_error_log)
155
+
156
+
157
+ if __name__ == '__main__':
158
+ unittest.main()