eval-framework 0.2.7__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.
- eval_framework/__init__.py +7 -0
- eval_framework/base_config.py +36 -0
- eval_framework/context/__init__.py +0 -0
- eval_framework/context/determined.py +177 -0
- eval_framework/context/eval.py +121 -0
- eval_framework/context/local.py +78 -0
- eval_framework/evaluation_generator.py +234 -0
- eval_framework/exceptions.py +2 -0
- eval_framework/external/ifeval_impl/README.md +5 -0
- eval_framework/external/ifeval_impl/instructions.py +1523 -0
- eval_framework/external/ifeval_impl/instructions_registry.py +161 -0
- eval_framework/external/ifeval_impl/instructions_util.py +1689 -0
- eval_framework/external/ifeval_impl/utils.py +135 -0
- eval_framework/llm/__init__.py +0 -0
- eval_framework/llm/aleph_alpha.py +432 -0
- eval_framework/llm/base.py +180 -0
- eval_framework/llm/huggingface.py +418 -0
- eval_framework/llm/mistral.py +88 -0
- eval_framework/llm/models.py +28 -0
- eval_framework/llm/openai.py +400 -0
- eval_framework/llm/vllm.py +554 -0
- eval_framework/logger.py +3 -0
- eval_framework/main.py +166 -0
- eval_framework/metrics/__init__.py +0 -0
- eval_framework/metrics/base.py +40 -0
- eval_framework/metrics/completion/__init__.py +1 -0
- eval_framework/metrics/completion/accuracy_completion.py +16 -0
- eval_framework/metrics/completion/aidanbench.py +28 -0
- eval_framework/metrics/completion/bleu.py +76 -0
- eval_framework/metrics/completion/chrf.py +62 -0
- eval_framework/metrics/completion/code_assertion.py +44 -0
- eval_framework/metrics/completion/code_execution_pass_at_one.py +126 -0
- eval_framework/metrics/completion/comet.py +56 -0
- eval_framework/metrics/completion/concordance_index.py +38 -0
- eval_framework/metrics/completion/csv_format.py +102 -0
- eval_framework/metrics/completion/cwe_accuracy.py +49 -0
- eval_framework/metrics/completion/exponential_similarity.py +65 -0
- eval_framework/metrics/completion/f1.py +42 -0
- eval_framework/metrics/completion/format_checker.py +56 -0
- eval_framework/metrics/completion/grid_difference.py +77 -0
- eval_framework/metrics/completion/ifeval.py +73 -0
- eval_framework/metrics/completion/json_format.py +179 -0
- eval_framework/metrics/completion/language_checker.py +74 -0
- eval_framework/metrics/completion/length_control.py +83 -0
- eval_framework/metrics/completion/math_reasoning_completion.py +307 -0
- eval_framework/metrics/completion/niah_accuracy.py +163 -0
- eval_framework/metrics/completion/placeholder_checker.py +27 -0
- eval_framework/metrics/completion/repetition.py +88 -0
- eval_framework/metrics/completion/rouge_1.py +35 -0
- eval_framework/metrics/completion/rouge_2.py +45 -0
- eval_framework/metrics/completion/rouge_geometric_mean.py +36 -0
- eval_framework/metrics/completion/rouge_l.py +52 -0
- eval_framework/metrics/completion/struct_eval_metrics.py +248 -0
- eval_framework/metrics/completion/ter.py +67 -0
- eval_framework/metrics/completion/text_counter.py +182 -0
- eval_framework/metrics/efficiency/__init__.py +0 -0
- eval_framework/metrics/efficiency/bytes_per_sequence_position.py +48 -0
- eval_framework/metrics/llm/__init__.py +0 -0
- eval_framework/metrics/llm/base.py +34 -0
- eval_framework/metrics/llm/graders/chatbot_style_grader.py +92 -0
- eval_framework/metrics/llm/graders/coherence_grader.py +115 -0
- eval_framework/metrics/llm/graders/comparison_grader.py +198 -0
- eval_framework/metrics/llm/graders/conciseness_grader.py +93 -0
- eval_framework/metrics/llm/graders/contains_names_grader.py +71 -0
- eval_framework/metrics/llm/graders/format_correctness_grader.py +109 -0
- eval_framework/metrics/llm/graders/instruction_grader.py +177 -0
- eval_framework/metrics/llm/graders/language.py +56 -0
- eval_framework/metrics/llm/graders/long_context_grader.py +72 -0
- eval_framework/metrics/llm/graders/models.py +74 -0
- eval_framework/metrics/llm/graders/refusal_grader.py +57 -0
- eval_framework/metrics/llm/graders/sql_quality_grader.py +145 -0
- eval_framework/metrics/llm/graders/summary_world_knowledge_grader.py +103 -0
- eval_framework/metrics/llm/llm_judge_chatbot_style.py +36 -0
- eval_framework/metrics/llm/llm_judge_coherence.py +44 -0
- eval_framework/metrics/llm/llm_judge_completion_accuracy.py +39 -0
- eval_framework/metrics/llm/llm_judge_conciseness.py +37 -0
- eval_framework/metrics/llm/llm_judge_contains_names.py +36 -0
- eval_framework/metrics/llm/llm_judge_format_correctness.py +43 -0
- eval_framework/metrics/llm/llm_judge_instruction.py +58 -0
- eval_framework/metrics/llm/llm_judge_mtbench_pair.py +306 -0
- eval_framework/metrics/llm/llm_judge_mtbench_single.py +210 -0
- eval_framework/metrics/llm/llm_judge_refusal.py +35 -0
- eval_framework/metrics/llm/llm_judge_sql.py +394 -0
- eval_framework/metrics/llm/llm_judge_world_knowledge.py +37 -0
- eval_framework/metrics/llm/utils.py +20 -0
- eval_framework/metrics/loglikelihood/__init__.py +0 -0
- eval_framework/metrics/loglikelihood/accuracy_loglikelihood.py +51 -0
- eval_framework/metrics/loglikelihood/base.py +50 -0
- eval_framework/metrics/loglikelihood/confidence_weighted_accuracy.py +25 -0
- eval_framework/metrics/loglikelihood/dcs.py +43 -0
- eval_framework/metrics/loglikelihood/probability_mass.py +53 -0
- eval_framework/metrics/loglikelihood/ternary.py +42 -0
- eval_framework/py.typed +0 -0
- eval_framework/response_generator.py +351 -0
- eval_framework/result_processors/__init__.py +0 -0
- eval_framework/result_processors/base.py +88 -0
- eval_framework/result_processors/hf_uploader.py +75 -0
- eval_framework/result_processors/result_processor.py +129 -0
- eval_framework/result_processors/wandb_uploader.py +137 -0
- eval_framework/run.py +369 -0
- eval_framework/run_direct.py +42 -0
- eval_framework/shared/types.py +227 -0
- eval_framework/tasks/__init__.py +6 -0
- eval_framework/tasks/base.py +392 -0
- eval_framework/tasks/benchmarks/__init__.py +0 -0
- eval_framework/tasks/benchmarks/aidanbench.py +211 -0
- eval_framework/tasks/benchmarks/arc.py +70 -0
- eval_framework/tasks/benchmarks/arc_de.py +46 -0
- eval_framework/tasks/benchmarks/arc_fi.py +46 -0
- eval_framework/tasks/benchmarks/belebele.py +60 -0
- eval_framework/tasks/benchmarks/bigcodebench.py +155 -0
- eval_framework/tasks/benchmarks/casehold.py +47 -0
- eval_framework/tasks/benchmarks/chembench.py +85 -0
- eval_framework/tasks/benchmarks/copa.py +64 -0
- eval_framework/tasks/benchmarks/duc.py +91 -0
- eval_framework/tasks/benchmarks/flores200.py +133 -0
- eval_framework/tasks/benchmarks/flores_plus.py +84 -0
- eval_framework/tasks/benchmarks/gpqa.py +201 -0
- eval_framework/tasks/benchmarks/gsm8k.py +150 -0
- eval_framework/tasks/benchmarks/hellaswag.py +69 -0
- eval_framework/tasks/benchmarks/hellaswag_de.py +52 -0
- eval_framework/tasks/benchmarks/humaneval.py +97 -0
- eval_framework/tasks/benchmarks/ifeval.py +78 -0
- eval_framework/tasks/benchmarks/include.py +119 -0
- eval_framework/tasks/benchmarks/infinitebench.py +302 -0
- eval_framework/tasks/benchmarks/math_reasoning.py +580 -0
- eval_framework/tasks/benchmarks/mbpp.py +192 -0
- eval_framework/tasks/benchmarks/mmlu.py +215 -0
- eval_framework/tasks/benchmarks/mmlu_de.py +109 -0
- eval_framework/tasks/benchmarks/mmlu_pro.py +164 -0
- eval_framework/tasks/benchmarks/mmmlu.py +529 -0
- eval_framework/tasks/benchmarks/openbookqa.py +85 -0
- eval_framework/tasks/benchmarks/opengptx_eu20.py +363 -0
- eval_framework/tasks/benchmarks/pawsx.py +65 -0
- eval_framework/tasks/benchmarks/piqa.py +64 -0
- eval_framework/tasks/benchmarks/quality.py +56 -0
- eval_framework/tasks/benchmarks/sciq.py +110 -0
- eval_framework/tasks/benchmarks/sphyr.py +79 -0
- eval_framework/tasks/benchmarks/squad.py +211 -0
- eval_framework/tasks/benchmarks/struct_eval.py +116 -0
- eval_framework/tasks/benchmarks/tablebench.py +117 -0
- eval_framework/tasks/benchmarks/triviaqa.py +42 -0
- eval_framework/tasks/benchmarks/truthfulqa.py +119 -0
- eval_framework/tasks/benchmarks/winogender.py +64 -0
- eval_framework/tasks/benchmarks/winogrande.py +69 -0
- eval_framework/tasks/benchmarks/winox.py +57 -0
- eval_framework/tasks/benchmarks/wmt.py +160 -0
- eval_framework/tasks/benchmarks/zero_scrolls.py +197 -0
- eval_framework/tasks/eval_config.py +136 -0
- eval_framework/tasks/perturbation.py +83 -0
- eval_framework/tasks/registry.py +186 -0
- eval_framework/tasks/task_loader.py +81 -0
- eval_framework/tasks/task_names.py +324 -0
- eval_framework/tasks/utils.py +584 -0
- eval_framework/utils/constants.py +9 -0
- eval_framework/utils/file_ops.py +245 -0
- eval_framework/utils/generate_task_docs.py +244 -0
- eval_framework/utils/helpers.py +32 -0
- eval_framework/utils/logging.py +62 -0
- eval_framework/utils/packaging.py +52 -0
- eval_framework/utils/tqdm_handler.py +14 -0
- eval_framework-0.2.7.dist-info/METADATA +548 -0
- eval_framework-0.2.7.dist-info/RECORD +170 -0
- eval_framework-0.2.7.dist-info/WHEEL +4 -0
- eval_framework-0.2.7.dist-info/entry_points.txt +3 -0
- template_formatting/README.md +83 -0
- template_formatting/__init__.py +0 -0
- template_formatting/formatter.py +537 -0
- template_formatting/mistral_formatter.py +159 -0
- template_formatting/py.typed +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import signal
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import FrameType
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import wandb
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WandbFs:
|
|
18
|
+
REGISTRY_MODEL_ROOT = "wandb-registry-model"
|
|
19
|
+
"""
|
|
20
|
+
WandbFs provides an interface to interact with Weights & Biases artifacts.
|
|
21
|
+
|
|
22
|
+
WandB provides a unified API to access artifacts with artifact.download().
|
|
23
|
+
|
|
24
|
+
Several issues with the standard WandB artifact handling motivated the creation of this class:
|
|
25
|
+
|
|
26
|
+
1. Artifacts may not always be in a HuggingFace-compatible format and may have extra directories.
|
|
27
|
+
This class includes methods to find HuggingFace checkpoints in downloaded artifacts.
|
|
28
|
+
|
|
29
|
+
2. Custom download paths with clean up upon failure: Rather than downloading to a
|
|
30
|
+
WandB-managed cache directory, this class allows users to specify a custom download path
|
|
31
|
+
(which can be a persistent directory). If no path is provided, a temporary directory is
|
|
32
|
+
created and cleaned up automatically.
|
|
33
|
+
|
|
34
|
+
3. Cleanup is handled in two ways, and exit handles are registered accordingly:
|
|
35
|
+
- when not used as a context manager atexit handlers ensure cleanup on normal script termination
|
|
36
|
+
- when used as a context manager, cleanup is handled in __exit__ directly
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
user_supplied_download_path: Optional path to download artifacts to. If not given, WANDB_ARTIFACT_DIR is used.
|
|
40
|
+
This acts as a cache, so if the artifact is already present, it will not be re-downloaded.
|
|
41
|
+
|
|
42
|
+
example usage:
|
|
43
|
+
>> with WandbFs("./my-download-path/") as wandb_fs:
|
|
44
|
+
.. artifact = wandb_fs.get_artifact("my-artifact", version="v1")
|
|
45
|
+
.. download_path = wandb_fs.download_artifact(artifact)
|
|
46
|
+
.. file_root = wandb_fs.find_hf_checkpoint_root_from_path_list()
|
|
47
|
+
|
|
48
|
+
>> wandb_fs = WandbFs("./my-download-path/")
|
|
49
|
+
.. wandb_fs.setup_cleanup_handlers()
|
|
50
|
+
.. artifact = wandb_fs.get_artifact("my-artifact", version="v1")
|
|
51
|
+
.. download_path = wandb_fs.download_artifact(artifact)
|
|
52
|
+
.. file_root = wandb_fs.find_hf_checkpoint_root_from_path_list()
|
|
53
|
+
.. wandb_fs.restore_cleanup_handlers()
|
|
54
|
+
.. some_function_that_uses_the_artifact(file_root)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, download_path: str | Path | None = None):
|
|
58
|
+
self.api = wandb.Api()
|
|
59
|
+
if download_path is None and os.getenv("WANDB_ARTIFACT_DIR") is not None:
|
|
60
|
+
download_path = os.getenv("WANDB_ARTIFACT_DIR")
|
|
61
|
+
self.user_supplied_download_path = Path(download_path) if download_path is not None else None
|
|
62
|
+
self._temp_dir: tempfile.TemporaryDirectory | None = None
|
|
63
|
+
self.download_path: Path | None = None
|
|
64
|
+
self._setup_s3_client()
|
|
65
|
+
|
|
66
|
+
def setup_cleanup_handlers(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
because wandbfs deals with downloading files, we will need to
|
|
69
|
+
make sure that at exit and at failure, the directory does not persist
|
|
70
|
+
|
|
71
|
+
these are present to ensure cleanup on normal script termination if not a context manager.
|
|
72
|
+
"""
|
|
73
|
+
# we only want to register the appropriate cleanup function
|
|
74
|
+
if self.user_supplied_download_path:
|
|
75
|
+
atexit.register(self._cleanup_user_dir)
|
|
76
|
+
else:
|
|
77
|
+
atexit.register(self._cleanup_temp_dir)
|
|
78
|
+
|
|
79
|
+
def restore_cleanup_handlers(self) -> None:
|
|
80
|
+
"""
|
|
81
|
+
unregister the cleanup handlers
|
|
82
|
+
"""
|
|
83
|
+
if self.user_supplied_download_path:
|
|
84
|
+
atexit.unregister(self._cleanup_user_dir)
|
|
85
|
+
else:
|
|
86
|
+
atexit.unregister(self._cleanup_temp_dir)
|
|
87
|
+
|
|
88
|
+
def _clean_on_signal(self, signum: int, frame: FrameType | None) -> None:
|
|
89
|
+
# we need to re-raise the signal to terminate gracefully with __exit__
|
|
90
|
+
# if we call cleanup directly, then the first time we try to rmtree
|
|
91
|
+
# we get an OSError
|
|
92
|
+
self.__exit__(None, None, None)
|
|
93
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
94
|
+
os.kill(os.getpid(), signum)
|
|
95
|
+
|
|
96
|
+
def _setup_s3_client(self) -> None:
|
|
97
|
+
required_env_vars = ["AWS_ENDPOINT_URL", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
|
98
|
+
for var in required_env_vars:
|
|
99
|
+
if var not in os.environ:
|
|
100
|
+
raise ValueError(f"Missing required environment variable: {var}")
|
|
101
|
+
endpoint = os.environ["AWS_ENDPOINT_URL"]
|
|
102
|
+
if not endpoint.startswith(("http://", "https://")):
|
|
103
|
+
os.environ["AWS_ENDPOINT_URL"] = f"https://{endpoint}"
|
|
104
|
+
|
|
105
|
+
def get_artifact(self, artifact_id: str, version: str = "latest") -> wandb.Artifact:
|
|
106
|
+
name = f"{artifact_id}:{version}"
|
|
107
|
+
if "/" not in artifact_id:
|
|
108
|
+
name = f"{self.REGISTRY_MODEL_ROOT}/{name}"
|
|
109
|
+
|
|
110
|
+
# Prefer checkpoints refering to local file system (speed & immediate availability)
|
|
111
|
+
local_name = name if name.endswith("-local") else f"{name}-local"
|
|
112
|
+
if self.api.artifact_exists(local_name):
|
|
113
|
+
artifact = self.api.artifact(local_name)
|
|
114
|
+
availabilities = []
|
|
115
|
+
for f in artifact.files():
|
|
116
|
+
local_path = Path(f.path_uri.removeprefix("file://"))
|
|
117
|
+
availabilities.append(local_path.exists() and local_path.stat().st_size == f.size)
|
|
118
|
+
if all(availabilities):
|
|
119
|
+
if local_name != name:
|
|
120
|
+
logger.info(f"Local artifact '{local_name}' available, using it instead of '{version}'.")
|
|
121
|
+
return artifact
|
|
122
|
+
|
|
123
|
+
# Otherwise fall back to the non-local version (requires download)
|
|
124
|
+
final_name = name.removesuffix("-local")
|
|
125
|
+
if final_name != name:
|
|
126
|
+
logger.info(f"Local artifact '{name}' NOT available, using '{final_name}' instead.")
|
|
127
|
+
|
|
128
|
+
if self.api.artifact_exists(local_name):
|
|
129
|
+
# Wait for a non-local artifact to pop-up, which is expected when a local one exists
|
|
130
|
+
end_time = int(os.getenv("WANDB_ARTIFACT_WAIT_TIMEOUT_SEC", "3600")) + time.time()
|
|
131
|
+
while end_time > time.time():
|
|
132
|
+
if self.api.artifact_exists(final_name):
|
|
133
|
+
return self.api.artifact(final_name)
|
|
134
|
+
logger.info(f"Artifact '{final_name}' not found, retrying in 30 seconds...")
|
|
135
|
+
time.sleep(30)
|
|
136
|
+
raise RuntimeError(f"Timed out waiting for {final_name}, perhaps increase WANDB_ARTIFACT_WAIT_TIMEOUT_SEC.")
|
|
137
|
+
else:
|
|
138
|
+
# Return artifact or crash if not found
|
|
139
|
+
return self.api.artifact(final_name)
|
|
140
|
+
|
|
141
|
+
def download_artifact(
|
|
142
|
+
self,
|
|
143
|
+
artifact: wandb.Artifact,
|
|
144
|
+
) -> Path:
|
|
145
|
+
"""
|
|
146
|
+
download_artifact downloads the specified artifact to either a user-specified
|
|
147
|
+
directory or a temporary directory. If the user-specified directory already
|
|
148
|
+
contains the artifact, it will not be re-downloaded.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
artifact: The WandB artifact object to download.
|
|
152
|
+
Returns:
|
|
153
|
+
Path: The path to the downloaded artifact.
|
|
154
|
+
"""
|
|
155
|
+
if artifact.qualified_name.endswith("-local"):
|
|
156
|
+
self.download_path = Path(
|
|
157
|
+
os.path.commonpath([Path(f.path_uri.removeprefix("file://")) for f in artifact.files()])
|
|
158
|
+
)
|
|
159
|
+
return self.download_path if self.download_path.is_dir() else self.download_path.parent
|
|
160
|
+
|
|
161
|
+
# create the base path for either a temp or user dir
|
|
162
|
+
if self.user_supplied_download_path is None:
|
|
163
|
+
self._temp_dir = tempfile.TemporaryDirectory()
|
|
164
|
+
base_path = Path(self._temp_dir.name)
|
|
165
|
+
else:
|
|
166
|
+
base_path = self.user_supplied_download_path
|
|
167
|
+
|
|
168
|
+
artifact_subdir = "/".join(artifact.name.split(":"))
|
|
169
|
+
self.download_path = base_path / artifact_subdir
|
|
170
|
+
if self.user_supplied_download_path and self.download_path.exists():
|
|
171
|
+
return self.download_path
|
|
172
|
+
|
|
173
|
+
logger.info(f"Downloading artifact to {self.download_path}")
|
|
174
|
+
# Since the cache lives inside the docker container, it is unused in future
|
|
175
|
+
# runs. Skipping the cache also avoids file duplication and extra copying.
|
|
176
|
+
self._artifact_downloaded = False
|
|
177
|
+
skip_cache = os.getenv("WANDB_CACHE_SKIP", "true").lower() in ["true", "1", "yes"]
|
|
178
|
+
artifact_path = artifact.download(root=str(self.download_path), skip_cache=skip_cache)
|
|
179
|
+
self._artifact_downloaded = True
|
|
180
|
+
|
|
181
|
+
return Path(artifact_path)
|
|
182
|
+
|
|
183
|
+
def find_hf_checkpoint_root_from_path_list(self) -> Path | None:
|
|
184
|
+
"""Find HuggingFace checkpoint root from a list of file paths.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
file_paths: List of file paths (can be S3 URIs or local paths)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
str | None: Path to the HuggingFace checkpoint root folder, or None if not found
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
# self.download_path can be None if download_artifact was never called
|
|
194
|
+
if self.download_path and self.download_path.exists():
|
|
195
|
+
checkpoint_roots = [x for x in Path(self.download_path).glob("**/config.json")]
|
|
196
|
+
if checkpoint_roots:
|
|
197
|
+
assert len(checkpoint_roots) == 1, "Multiple checkpoints found"
|
|
198
|
+
logger.info(f"Checkpoint found in {checkpoint_roots[0].parent}.")
|
|
199
|
+
return checkpoint_roots[0].parent
|
|
200
|
+
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def __enter__(self) -> "WandbFs":
|
|
204
|
+
self._original_sigterm = signal.signal(signal.SIGTERM, self._clean_on_signal)
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
208
|
+
"""
|
|
209
|
+
exit the context manager, cleaning up the temporary directory if it was used
|
|
210
|
+
or the user directory if it was specified and failed to download.
|
|
211
|
+
"""
|
|
212
|
+
if self.user_supplied_download_path:
|
|
213
|
+
self._cleanup_user_dir()
|
|
214
|
+
else:
|
|
215
|
+
self._cleanup_temp_dir()
|
|
216
|
+
|
|
217
|
+
signal.signal(signal.SIGTERM, self._original_sigterm)
|
|
218
|
+
|
|
219
|
+
def _cleanup_user_dir(self) -> None:
|
|
220
|
+
"""
|
|
221
|
+
_cleanup_user_dir will remove the contents of the user specified
|
|
222
|
+
download path if there was an attempt to download the artifact and it failed.
|
|
223
|
+
"""
|
|
224
|
+
if (
|
|
225
|
+
# check to make sure this flag was set; if not, there was no attempt to download.
|
|
226
|
+
not getattr(self, "_artifact_downloaded", True)
|
|
227
|
+
and self.user_supplied_download_path
|
|
228
|
+
and self.download_path
|
|
229
|
+
and self.download_path.exists()
|
|
230
|
+
):
|
|
231
|
+
# remove the contents of the download path.
|
|
232
|
+
logger.info(f"Cleaning up user-specified download path...{self.download_path}")
|
|
233
|
+
# ignore errors because the directory is not empty
|
|
234
|
+
shutil.rmtree(self.download_path, ignore_errors=True)
|
|
235
|
+
|
|
236
|
+
def _cleanup_temp_dir(self) -> None:
|
|
237
|
+
if hasattr(self, "_temp_dir") and self._temp_dir is not None:
|
|
238
|
+
try:
|
|
239
|
+
self._temp_dir.cleanup()
|
|
240
|
+
except (OSError, FileNotFoundError):
|
|
241
|
+
# Directory might already be cleaned up or removed
|
|
242
|
+
pass
|
|
243
|
+
finally:
|
|
244
|
+
self._temp_dir = None
|
|
245
|
+
self.download_path = None
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import tqdm
|
|
8
|
+
|
|
9
|
+
from eval_framework.tasks.registry import get_task, registered_task_names
|
|
10
|
+
from eval_framework.tasks.task_loader import load_extra_tasks
|
|
11
|
+
from template_formatting.formatter import BaseFormatter, ConcatFormatter, Llama3Formatter
|
|
12
|
+
|
|
13
|
+
DEFAULT_OUTPUT_DOCS_DIRECTORY = Path("docs/tasks")
|
|
14
|
+
|
|
15
|
+
EXCLUDED_TASKS: list[str] = []
|
|
16
|
+
|
|
17
|
+
# Base URL for the main repository to ensure links work even in external/companion repos
|
|
18
|
+
REPO_URL = "https://github.com/Aleph-Alpha-Research/eval-framework/blob/main"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_args(cli_args: list[str] | None = None) -> argparse.Namespace:
|
|
22
|
+
"""Parse command line arguments for the script."""
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser()
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--add-prompt-examples",
|
|
27
|
+
action="store_true",
|
|
28
|
+
default=False,
|
|
29
|
+
required=False,
|
|
30
|
+
help="If set, examples prompts for each of the formatters will be added in the generated docs.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--exclude-tasks",
|
|
34
|
+
nargs="*",
|
|
35
|
+
type=str,
|
|
36
|
+
default=[],
|
|
37
|
+
required=False,
|
|
38
|
+
help="List of task names to exclude from documentation generation.",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--extra-task-modules",
|
|
42
|
+
nargs="*",
|
|
43
|
+
type=str,
|
|
44
|
+
default=[],
|
|
45
|
+
required=False,
|
|
46
|
+
help="List of files and folders containing additional task definitions.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--formatter",
|
|
50
|
+
nargs="*",
|
|
51
|
+
type=str,
|
|
52
|
+
required=False,
|
|
53
|
+
default=["ConcatFormatter", "Llama3Formatter"],
|
|
54
|
+
help="Specify which formatter to use for formatting the task samples. "
|
|
55
|
+
"If not explicitly specified, default formatters will be used.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--only-tasks",
|
|
59
|
+
nargs="*",
|
|
60
|
+
type=str,
|
|
61
|
+
default=[],
|
|
62
|
+
required=False,
|
|
63
|
+
help="List of task names to generate documentation for. If empty, all tasks will be processed.",
|
|
64
|
+
)
|
|
65
|
+
return parser.parse_args(args=cli_args)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate_docs_for_task(
|
|
69
|
+
output_docs_directory: Path, task_name: str, formatters: list[BaseFormatter], add_prompt_examples: bool
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Generate documentation for a specific task."""
|
|
72
|
+
task_class = get_task(task_name)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
num_fewshot = 1
|
|
76
|
+
task = task_class(num_fewshot=num_fewshot)
|
|
77
|
+
except Exception:
|
|
78
|
+
try:
|
|
79
|
+
num_fewshot = 0
|
|
80
|
+
task = task_class(num_fewshot=num_fewshot)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Failed to instantiate task {task_name}: {e}")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
with open(f"{output_docs_directory}/{task_name}.md", "w") as f:
|
|
86
|
+
f.write(f"# {task_name}\n\n")
|
|
87
|
+
http_path = f"https://huggingface.co/datasets/{task.DATASET_PATH}" if task.DATASET_PATH else None
|
|
88
|
+
|
|
89
|
+
f.write("````\n") # fence with 4 thicks because some prompts have code blocks with 3 thicks
|
|
90
|
+
f.write(f"NAME = {task_name}".strip() + "\n")
|
|
91
|
+
if hasattr(task, "DATASET_PATH"):
|
|
92
|
+
f.write(f"DATASET_PATH = {task.DATASET_PATH}".strip() + "\n")
|
|
93
|
+
if hasattr(task, "SAMPLE_SPLIT"):
|
|
94
|
+
f.write(f"SAMPLE_SPLIT = {task.SAMPLE_SPLIT}".strip() + "\n")
|
|
95
|
+
if hasattr(task, "FEWSHOT_SPLIT"):
|
|
96
|
+
f.write(f"FEWSHOT_SPLIT = {task.FEWSHOT_SPLIT}".strip() + "\n")
|
|
97
|
+
if hasattr(task, "RESPONSE_TYPE"):
|
|
98
|
+
f.write(f"RESPONSE_TYPE = {task.RESPONSE_TYPE.name}".strip() + "\n")
|
|
99
|
+
if hasattr(task, "METRICS"):
|
|
100
|
+
metrics_list = [f"{m.__name__}" for m in task.METRICS]
|
|
101
|
+
f.write(f"METRICS = [{', '.join(metrics_list)}]".strip() + "\n")
|
|
102
|
+
if hasattr(task, "SUBJECTS"):
|
|
103
|
+
f.write(f"SUBJECTS = {repr(task.SUBJECTS)}".strip() + "\n")
|
|
104
|
+
if hasattr(task, "LANGUAGE"):
|
|
105
|
+
f.write(f"LANGUAGE = {repr(task.LANGUAGE)}".strip() + "\n")
|
|
106
|
+
f.write("````\n\n")
|
|
107
|
+
|
|
108
|
+
f.write(f"- Module: `{task_class.__module__}`\n\n")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
raw_file_path = inspect.getfile(task_class)
|
|
112
|
+
# Find the package root 'eval_framework' in the path
|
|
113
|
+
match = re.search(r"eval_framework.*", raw_file_path)
|
|
114
|
+
if match:
|
|
115
|
+
# Reconstruct relative path assuming standard 'src' structure
|
|
116
|
+
task_file = f"src/{match.group(0)}"
|
|
117
|
+
# Provide a local relative link (for VS Code) and an absolute link (for GitHub/Web)
|
|
118
|
+
f.write(f"- File: [{task_file}](../../{task_file}) | [View on GitHub]({REPO_URL}/{task_file})\n\n")
|
|
119
|
+
else:
|
|
120
|
+
# Fallback for tasks defined outside the main package (e.g., custom local tasks)
|
|
121
|
+
f.write(f"- File: `{raw_file_path}`\n\n")
|
|
122
|
+
except Exception:
|
|
123
|
+
f.write("- File: `Dynamic or Built-in`\n\n")
|
|
124
|
+
|
|
125
|
+
if http_path:
|
|
126
|
+
f.write(f"- Link to dataset: [{http_path}]({http_path})\n\n")
|
|
127
|
+
|
|
128
|
+
if not add_prompt_examples:
|
|
129
|
+
f.write(
|
|
130
|
+
f"More detailed documentation, with prompt examples and ground truth completions, can be generated "
|
|
131
|
+
f"with `uv run -m eval_framework.utils.generate_task_docs --add-prompt-examples "
|
|
132
|
+
f'--only-tasks "{task_name}"`.\n'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
s = next(iter(task.iterate_samples(1)))
|
|
137
|
+
for split in task.dataset:
|
|
138
|
+
f.write(f"- `{split}` has {len(task.dataset[split])} samples\n\n")
|
|
139
|
+
|
|
140
|
+
for formatter in formatters:
|
|
141
|
+
f.write(f"## Example prompt with {formatter.__class__.__name__} ({num_fewshot}-shot)\n\n")
|
|
142
|
+
formatted_sample = formatter.format(s.messages, output_mode="string")
|
|
143
|
+
f.write("````\n")
|
|
144
|
+
f.write(f'"{formatted_sample}"')
|
|
145
|
+
f.write("\n````\n\n")
|
|
146
|
+
|
|
147
|
+
f.write("## Possible completions:\n\n")
|
|
148
|
+
f.write("````\n")
|
|
149
|
+
if s.possible_completions:
|
|
150
|
+
for item in (
|
|
151
|
+
s.possible_completions if isinstance(s.possible_completions, list) else [s.possible_completions]
|
|
152
|
+
):
|
|
153
|
+
f.write(f'- "{item}"\n')
|
|
154
|
+
else:
|
|
155
|
+
f.write("None\n")
|
|
156
|
+
f.write("````\n\n")
|
|
157
|
+
|
|
158
|
+
f.write("## Ground truth:\n\n")
|
|
159
|
+
f.write("````\n")
|
|
160
|
+
if s.ground_truth:
|
|
161
|
+
for item in s.ground_truth if isinstance(s.ground_truth, list) else [s.ground_truth]:
|
|
162
|
+
f.write(f'- "{item}"\n')
|
|
163
|
+
else:
|
|
164
|
+
f.write("None\n")
|
|
165
|
+
f.write("````\n")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def generate_readme_list(output_docs_directory: Path, total_tasks: int) -> None:
|
|
169
|
+
"""Generate a README file listing all tasks with total count."""
|
|
170
|
+
|
|
171
|
+
with open(f"{output_docs_directory}/README.md", "w") as f:
|
|
172
|
+
f.write(
|
|
173
|
+
"# Task documentation\n\n"
|
|
174
|
+
"This directory contains the generated documentation for all benchmark tasks available in the package.\n\n"
|
|
175
|
+
f"**Total number of tasks: {total_tasks}**\n\n"
|
|
176
|
+
"The documentation can be generated or updated with "
|
|
177
|
+
"`uv run -m eval_framework.utils.generate_task_docs`.\n\n"
|
|
178
|
+
"NOTE: This is an automatically generated file. Any manual modifications will not be preserved when "
|
|
179
|
+
"the file is updated.\n\n"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
f.write("## List of tasks\n\n")
|
|
183
|
+
# sort files alphabetically and ignore README.md
|
|
184
|
+
for file in sorted(os.listdir(output_docs_directory)):
|
|
185
|
+
if file.endswith(".md") and file != "README.md":
|
|
186
|
+
task_name = file[:-3]
|
|
187
|
+
f.write(f"- [{task_name}]({task_name}.md)\n")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def generate_all_docs(args: argparse.Namespace, output_docs_directory: Path) -> None:
|
|
191
|
+
# Load extra tasks if specified
|
|
192
|
+
if args.extra_task_modules:
|
|
193
|
+
print(f"Loading extra tasks from: {args.extra_task_modules}")
|
|
194
|
+
load_extra_tasks(args.extra_task_modules)
|
|
195
|
+
|
|
196
|
+
# List the tasks to process
|
|
197
|
+
filtered_tasks = []
|
|
198
|
+
for task_name in registered_task_names():
|
|
199
|
+
if args.only_tasks and task_name not in args.only_tasks:
|
|
200
|
+
continue
|
|
201
|
+
if task_name in args.exclude_tasks or task_name in EXCLUDED_TASKS:
|
|
202
|
+
continue
|
|
203
|
+
filtered_tasks.append(task_name)
|
|
204
|
+
filtered_tasks.sort()
|
|
205
|
+
|
|
206
|
+
print(f"Found {len(filtered_tasks)} tasks to process: {', '.join([task_name for task_name in filtered_tasks])}")
|
|
207
|
+
|
|
208
|
+
# List the formatters to use
|
|
209
|
+
supported_formatters = {f.__class__.__name__: f for f in [ConcatFormatter(), Llama3Formatter()]}
|
|
210
|
+
formatters = []
|
|
211
|
+
for f in args.formatter:
|
|
212
|
+
if f in supported_formatters:
|
|
213
|
+
formatters.append(supported_formatters[f])
|
|
214
|
+
else:
|
|
215
|
+
raise ValueError(f"Unsupported formatter: {f}")
|
|
216
|
+
|
|
217
|
+
# Create the output directory if it does not exist
|
|
218
|
+
os.makedirs(output_docs_directory, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
for task_name in tqdm.tqdm(filtered_tasks, desc="Generating documentation for tasks"):
|
|
221
|
+
try:
|
|
222
|
+
generate_docs_for_task(
|
|
223
|
+
output_docs_directory=output_docs_directory,
|
|
224
|
+
task_name=task_name,
|
|
225
|
+
formatters=formatters,
|
|
226
|
+
add_prompt_examples=args.add_prompt_examples,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print("---")
|
|
231
|
+
print(f"failed generating documentation for task {task_name}: {e}")
|
|
232
|
+
file_path = f"{output_docs_directory}/{task_name}.md"
|
|
233
|
+
if os.path.exists(file_path):
|
|
234
|
+
os.remove(file_path)
|
|
235
|
+
print("---")
|
|
236
|
+
|
|
237
|
+
# Pass the total number of processed tasks to the README generator
|
|
238
|
+
generate_readme_list(output_docs_directory=output_docs_directory, total_tasks=len(filtered_tasks))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
print("Generating task documentation...")
|
|
243
|
+
args = parse_args()
|
|
244
|
+
generate_all_docs(args, output_docs_directory=DEFAULT_OUTPUT_DOCS_DIRECTORY)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def count_bytes(text: str, /, *, encoding: str = "utf-8") -> int:
|
|
5
|
+
"""Count the number of bytes in a string."""
|
|
6
|
+
return len(bytes(text.encode(encoding)))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cosine_similarity(embedding_a: list[float], embedding_b: list[float]) -> float:
|
|
10
|
+
"""Computes the cosine similarity between two embeddings."""
|
|
11
|
+
return pairwise_cosine_similarity([embedding_a], [embedding_b])[0][0]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pairwise_cosine_similarity(embeddings_a: list[list[float]], embeddings_b: list[list[float]]) -> list[list[float]]:
|
|
15
|
+
"""
|
|
16
|
+
Computes the pairwise cosine similarity matrix between two lists of embeddings.
|
|
17
|
+
Output[i][j] is the cosine similarity between embeddings_a[i] and embeddings_b[j].
|
|
18
|
+
"""
|
|
19
|
+
# M, D = len(embeddings_a), len(embeddings_a[0])
|
|
20
|
+
# N, D = len(embeddings_b), len(embeddings_b[0])
|
|
21
|
+
A = np.array(embeddings_a) # shape (M, D)
|
|
22
|
+
B = np.array(embeddings_b) # shape (N, D)
|
|
23
|
+
|
|
24
|
+
norms_a = np.linalg.norm(A, axis=1, keepdims=True)
|
|
25
|
+
norms_b = np.linalg.norm(B, axis=1, keepdims=True)
|
|
26
|
+
|
|
27
|
+
epsilon = 1e-9
|
|
28
|
+
A_normalized = A / (norms_a + epsilon)
|
|
29
|
+
B_normalized = B / (norms_b + epsilon)
|
|
30
|
+
|
|
31
|
+
similarity_matrix = A_normalized @ B_normalized.T # (M, D) @ (D, N) -> (M, N)
|
|
32
|
+
return similarity_matrix.tolist()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
VERBOSITY_MAP = {
|
|
6
|
+
0: logging.CRITICAL,
|
|
7
|
+
1: logging.INFO,
|
|
8
|
+
2: logging.DEBUG,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logging(
|
|
13
|
+
output_dir: Path | None = None, log_level: int = 1, log_filename: str = "evaluation.log"
|
|
14
|
+
) -> logging.Logger:
|
|
15
|
+
"""
|
|
16
|
+
Set up centralized logging configuration for the entire framework.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
output_dir: Directory to save log files. If None, logs only to console.
|
|
20
|
+
log_level: Logging level (default: INFO)
|
|
21
|
+
log_filename: Name of the log file
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Configured root logger
|
|
25
|
+
"""
|
|
26
|
+
# Map verbosity integer to logging level
|
|
27
|
+
mapped_log_level = VERBOSITY_MAP.get(log_level, logging.INFO)
|
|
28
|
+
|
|
29
|
+
# Basic configuration
|
|
30
|
+
logging.basicConfig(level=mapped_log_level)
|
|
31
|
+
|
|
32
|
+
# Create formatter
|
|
33
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
34
|
+
|
|
35
|
+
# Get root logger and clear any existing handlers
|
|
36
|
+
root_logger = logging.getLogger()
|
|
37
|
+
root_logger.handlers.clear()
|
|
38
|
+
root_logger.setLevel(mapped_log_level)
|
|
39
|
+
|
|
40
|
+
# Console handler (always present)
|
|
41
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
42
|
+
console_handler.setLevel(mapped_log_level)
|
|
43
|
+
console_handler.setFormatter(formatter)
|
|
44
|
+
root_logger.addHandler(console_handler)
|
|
45
|
+
|
|
46
|
+
# File handler (if output directory provided)
|
|
47
|
+
if output_dir:
|
|
48
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
log_file = output_dir / log_filename
|
|
50
|
+
|
|
51
|
+
file_handler = logging.FileHandler(log_file, mode="w")
|
|
52
|
+
file_handler.setLevel(mapped_log_level)
|
|
53
|
+
file_handler.setFormatter(formatter)
|
|
54
|
+
root_logger.addHandler(file_handler)
|
|
55
|
+
|
|
56
|
+
root_logger.info(f"Logging configured. File: {log_file}")
|
|
57
|
+
else:
|
|
58
|
+
root_logger.info("Logging configured (console only)")
|
|
59
|
+
|
|
60
|
+
root_logger.info(f"Output directory for logs: {output_dir if output_dir else 'None'}")
|
|
61
|
+
|
|
62
|
+
return root_logger
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from packaging.requirements import Requirement
|
|
6
|
+
from packaging.version import Version
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_package_extras(extras: str | Sequence[str], /, *, package: str = "eval_framework") -> Sequence[str]:
|
|
10
|
+
"""Validate that the specified extras are valid for the given package."""
|
|
11
|
+
if isinstance(extras, str):
|
|
12
|
+
extras = [extras]
|
|
13
|
+
|
|
14
|
+
metadata = importlib.metadata.metadata(package)
|
|
15
|
+
package_extras = set(metadata.get_all("Provides-Extra") or [])
|
|
16
|
+
for extra in extras:
|
|
17
|
+
if extra not in package_extras:
|
|
18
|
+
raise ValueError(f"Invalid extra: {extra}. Options are {package_extras}")
|
|
19
|
+
|
|
20
|
+
return extras
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extra_requires(extra: str, /, *, package: str = "eval_framework") -> list[str]:
|
|
24
|
+
"""Return a list of requirements for the specified extra."""
|
|
25
|
+
validate_package_extras(extra, package=package)
|
|
26
|
+
dist = importlib.metadata.distribution(package)
|
|
27
|
+
requires = dist.requires or []
|
|
28
|
+
extra_str = f"extra == '{extra}'"
|
|
29
|
+
return [r.split(";")[0].strip() for r in requires if r.endswith(extra_str)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _dependency_satisfied(dep: str, /) -> bool:
|
|
33
|
+
"""Return True if the dependency string is satisfied.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
A dependency string: for example "torch~=2.0".
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
dist = importlib.metadata.distribution(Requirement(dep).name)
|
|
40
|
+
installed_version = Version(dist.version)
|
|
41
|
+
req = Requirement(dep)
|
|
42
|
+
return installed_version in req.specifier
|
|
43
|
+
except (importlib.metadata.PackageNotFoundError, Exception):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_extra_installed(extra: str, package: str = "eval_framework") -> bool:
|
|
48
|
+
"""Return `True` if all dependencies for a given extra are installed."""
|
|
49
|
+
for req in extra_requires(extra, package=package):
|
|
50
|
+
if not _dependency_satisfied(req):
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safe_tqdm_write(msg: str, level: int = logging.INFO) -> None:
|
|
9
|
+
if logger.isEnabledFor(level):
|
|
10
|
+
tqdm.write(msg)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_disable_bar_flag() -> bool:
|
|
14
|
+
return logger.getEffectiveLevel() >= logging.WARNING
|