lemonade-sdk 7.0.0__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.
Potentially problematic release.
This version of lemonade-sdk might be problematic. Click here for more details.
- lemonade/__init__.py +5 -0
- lemonade/api.py +125 -0
- lemonade/cache.py +85 -0
- lemonade/cli.py +135 -0
- lemonade/common/__init__.py +0 -0
- lemonade/common/analyze_model.py +26 -0
- lemonade/common/build.py +223 -0
- lemonade/common/cli_helpers.py +139 -0
- lemonade/common/exceptions.py +98 -0
- lemonade/common/filesystem.py +368 -0
- lemonade/common/labels.py +61 -0
- lemonade/common/onnx_helpers.py +176 -0
- lemonade/common/plugins.py +10 -0
- lemonade/common/printing.py +110 -0
- lemonade/common/status.py +490 -0
- lemonade/common/system_info.py +390 -0
- lemonade/common/tensor_helpers.py +83 -0
- lemonade/common/test_helpers.py +28 -0
- lemonade/profilers/__init__.py +1 -0
- lemonade/profilers/memory_tracker.py +257 -0
- lemonade/profilers/profiler.py +55 -0
- lemonade/sequence.py +363 -0
- lemonade/state.py +159 -0
- lemonade/tools/__init__.py +1 -0
- lemonade/tools/adapter.py +104 -0
- lemonade/tools/bench.py +284 -0
- lemonade/tools/huggingface_bench.py +267 -0
- lemonade/tools/huggingface_load.py +520 -0
- lemonade/tools/humaneval.py +258 -0
- lemonade/tools/llamacpp.py +261 -0
- lemonade/tools/llamacpp_bench.py +154 -0
- lemonade/tools/management_tools.py +273 -0
- lemonade/tools/mmlu.py +327 -0
- lemonade/tools/ort_genai/__init__.py +0 -0
- lemonade/tools/ort_genai/oga.py +1129 -0
- lemonade/tools/ort_genai/oga_bench.py +142 -0
- lemonade/tools/perplexity.py +146 -0
- lemonade/tools/prompt.py +228 -0
- lemonade/tools/quark/__init__.py +0 -0
- lemonade/tools/quark/quark_load.py +172 -0
- lemonade/tools/quark/quark_quantize.py +439 -0
- lemonade/tools/report/__init__.py +0 -0
- lemonade/tools/report/llm_report.py +203 -0
- lemonade/tools/report/table.py +739 -0
- lemonade/tools/server/__init__.py +0 -0
- lemonade/tools/server/serve.py +1354 -0
- lemonade/tools/server/tool_calls.py +146 -0
- lemonade/tools/tool.py +374 -0
- lemonade/version.py +1 -0
- lemonade_install/__init__.py +1 -0
- lemonade_install/install.py +774 -0
- lemonade_sdk-7.0.0.dist-info/METADATA +116 -0
- lemonade_sdk-7.0.0.dist-info/RECORD +61 -0
- lemonade_sdk-7.0.0.dist-info/WHEEL +5 -0
- lemonade_sdk-7.0.0.dist-info/entry_points.txt +4 -0
- lemonade_sdk-7.0.0.dist-info/licenses/LICENSE +201 -0
- lemonade_sdk-7.0.0.dist-info/licenses/NOTICE.md +21 -0
- lemonade_sdk-7.0.0.dist-info/top_level.txt +3 -0
- lemonade_server/cli.py +260 -0
- lemonade_server/model_manager.py +98 -0
- lemonade_server/server_models.json +142 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Dict
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_code_block(text):
|
|
8
|
+
"""
|
|
9
|
+
Extracts the content inside triple backtick code blocks from a text.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
text (str): The text to extract the code block from.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: The content of the first code block if any are found, otherwise the raw text.
|
|
16
|
+
"""
|
|
17
|
+
# Regex pattern to match triple backtick code blocks (with optional language hint)
|
|
18
|
+
pattern = re.compile(r"```(?:\w+)?\n(.*?)```", re.DOTALL)
|
|
19
|
+
|
|
20
|
+
# Find all matches
|
|
21
|
+
code_blocks = pattern.findall(text)
|
|
22
|
+
|
|
23
|
+
# Return first match or raw text
|
|
24
|
+
return code_blocks[0] if code_blocks else text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def standardize_tool_call(tool_call: dict) -> dict | None:
|
|
28
|
+
"""
|
|
29
|
+
Standardizes the format of tool calls according to the format expected by OpenAI.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
tool_call (dict): The tool call to validate.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
dict | None: Standardized tool call if valid, None otherwise.
|
|
36
|
+
"""
|
|
37
|
+
# Ensure the tool call has a "name"
|
|
38
|
+
standardized_tool_call = {}
|
|
39
|
+
if "name" in tool_call:
|
|
40
|
+
standardized_tool_call["name"] = tool_call["name"]
|
|
41
|
+
else:
|
|
42
|
+
logging.warning("Tool call does not have a 'name' field.")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
# Ensure the tool call has "arguments"
|
|
46
|
+
if "arguments" in tool_call:
|
|
47
|
+
standardized_tool_call["arguments"] = tool_call["arguments"]
|
|
48
|
+
elif "parameters" in tool_call:
|
|
49
|
+
standardized_tool_call["arguments"] = tool_call["parameters"]
|
|
50
|
+
else:
|
|
51
|
+
logging.warning("Tool call does not have a 'arguments' or 'parameters' field.")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
return standardized_tool_call
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_tool_calls(
|
|
58
|
+
text: str, added_tokens_decoder: List[str]
|
|
59
|
+
) -> tuple[List[Dict], str]:
|
|
60
|
+
"""
|
|
61
|
+
Extracts tool calls from generated text based on tool calling identifiers.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
text (str): The text output generated by the model.
|
|
65
|
+
added_tokens_decoder (List[str]): The list of tokens in the tokenizer.added_tokens_decoder.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
tuple[List[Dict], str]: A tuple containing:
|
|
69
|
+
- List[Dict]: A list of extracted tool call objects (raw JSON-like dicts)
|
|
70
|
+
- str: The original text with tool calls removed
|
|
71
|
+
"""
|
|
72
|
+
matches = []
|
|
73
|
+
special_tokens = [v.content for v in added_tokens_decoder.values()]
|
|
74
|
+
|
|
75
|
+
# Pattern 1: <tool_call>...</tool_call> block
|
|
76
|
+
# Sample model that uses this pattern: Qwen3-8B
|
|
77
|
+
if "<tool_call>" in special_tokens and "</tool_call>" in special_tokens:
|
|
78
|
+
tool_call_pattern = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
|
|
79
|
+
matches = list(tool_call_pattern.finditer(text))
|
|
80
|
+
|
|
81
|
+
# Pattern 2: [TOOL_CALLS] [ {...} ] block
|
|
82
|
+
# Sample model that uses this pattern: Mistral-7B-Instruct-v0.3
|
|
83
|
+
elif "[TOOL_CALLS]" in special_tokens:
|
|
84
|
+
tool_call_pattern = re.compile(
|
|
85
|
+
r"\[TOOL_CALLS\]\s*\[(.*?)\](?=\s*<|/?eos|$)", re.DOTALL
|
|
86
|
+
)
|
|
87
|
+
matches = list(tool_call_pattern.finditer(text))
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
logging.warning(
|
|
91
|
+
"Tool calling identifiers were not found for the current model."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Some models don't use any tool calling identifiers.
|
|
95
|
+
# Instead, tool calls are identified by only generating JSON content.
|
|
96
|
+
# Sample model that uses this pattern: Llama-3.1-8B-Instruct
|
|
97
|
+
try:
|
|
98
|
+
# Remove the json for a code block if needed
|
|
99
|
+
parsed_text = extract_code_block(text)
|
|
100
|
+
json_tool_calls = json.loads(parsed_text)
|
|
101
|
+
|
|
102
|
+
if isinstance(json_tool_calls, dict):
|
|
103
|
+
json_tool_calls = [json_tool_calls]
|
|
104
|
+
|
|
105
|
+
extracted_tool_calls = []
|
|
106
|
+
for tool_call in json_tool_calls:
|
|
107
|
+
# Return the tool call if all calls are valid
|
|
108
|
+
standard_tool_call = standardize_tool_call(tool_call)
|
|
109
|
+
if standard_tool_call is not None:
|
|
110
|
+
extracted_tool_calls.append(standard_tool_call)
|
|
111
|
+
else:
|
|
112
|
+
return [], text
|
|
113
|
+
|
|
114
|
+
return extracted_tool_calls, ""
|
|
115
|
+
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
# Process matches in reverse to avoid position shifting
|
|
120
|
+
extracted_tool_calls = []
|
|
121
|
+
cleaned_text = text
|
|
122
|
+
for match in reversed(matches):
|
|
123
|
+
content = match.group(1).strip()
|
|
124
|
+
json_tool_call = None
|
|
125
|
+
try:
|
|
126
|
+
json_tool_call = json.loads(content)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
logging.warning("Could not parse tool call as JSON.")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Attempt to standardize the tool call
|
|
132
|
+
standard_tool_call = standardize_tool_call(json_tool_call)
|
|
133
|
+
if standard_tool_call is None:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# If the content is a valid JSON object, add it to the list
|
|
137
|
+
extracted_tool_calls.append(standard_tool_call)
|
|
138
|
+
|
|
139
|
+
# Remove the matched tool call from the text
|
|
140
|
+
cleaned_text = cleaned_text[: match.start()] + cleaned_text[match.end() :]
|
|
141
|
+
|
|
142
|
+
return extracted_tool_calls, cleaned_text.strip()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
146
|
+
# Modifications Copyright (c) 2025 AMD
|
lemonade/tools/tool.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import argparse
|
|
6
|
+
import textwrap as _textwrap
|
|
7
|
+
import re
|
|
8
|
+
from typing import Tuple, Dict
|
|
9
|
+
from multiprocessing import Process, Queue
|
|
10
|
+
import psutil
|
|
11
|
+
import lemonade.common.printing as printing
|
|
12
|
+
import lemonade.common.exceptions as exp
|
|
13
|
+
import lemonade.common.build as build
|
|
14
|
+
import lemonade.common.filesystem as fs
|
|
15
|
+
from lemonade.state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _spinner(message, q: Queue):
|
|
19
|
+
"""
|
|
20
|
+
Displays a moving "..." indicator so that the user knows that the
|
|
21
|
+
Tool is still working. Tools can optionally use a multiprocessing
|
|
22
|
+
Queue to display the percent progress of the Tool.
|
|
23
|
+
"""
|
|
24
|
+
percent_complete = None
|
|
25
|
+
# Get sleep time from environment variable, default to 0.5s if not set
|
|
26
|
+
try:
|
|
27
|
+
sleep_time = float(os.getenv("LEMONADE_BUILD_MONITOR_FREQUENCY", "0.5"))
|
|
28
|
+
except ValueError:
|
|
29
|
+
sleep_time = 0.5
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
parent_process = psutil.Process(pid=os.getppid())
|
|
33
|
+
while parent_process.status() == psutil.STATUS_RUNNING:
|
|
34
|
+
for cursor in [" ", ". ", ".. ", "..."]:
|
|
35
|
+
time.sleep(sleep_time)
|
|
36
|
+
while not q.empty():
|
|
37
|
+
percent_complete = q.get()
|
|
38
|
+
if percent_complete is not None:
|
|
39
|
+
status = f" {message} ({percent_complete:.1f}%){cursor}\r"
|
|
40
|
+
else:
|
|
41
|
+
status = f" {message}{cursor} \r"
|
|
42
|
+
sys.stdout.write(status)
|
|
43
|
+
sys.stdout.flush()
|
|
44
|
+
except psutil.NoSuchProcess:
|
|
45
|
+
# If the parent process stopped existing, we can
|
|
46
|
+
# safely assume the spinner no longer needs to spin
|
|
47
|
+
# NOTE: this only seems to be needed on Windows
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _name_is_file_safe(name: str):
|
|
52
|
+
"""
|
|
53
|
+
Make sure the name can be used in a filename
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
allowed_in_unique_name = set(
|
|
57
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if len(name) == 0:
|
|
61
|
+
msg = """
|
|
62
|
+
Tool __init__() was passed a unique_name with no length. A
|
|
63
|
+
uniquely identifying unique_name is required.
|
|
64
|
+
"""
|
|
65
|
+
raise ValueError(msg)
|
|
66
|
+
|
|
67
|
+
for char in name:
|
|
68
|
+
if char not in allowed_in_unique_name:
|
|
69
|
+
msg = f"""
|
|
70
|
+
Tool __init__() was passed a unique_name:
|
|
71
|
+
{name}
|
|
72
|
+
with illegal characters. The unique_name must be safe to
|
|
73
|
+
use in a filename, meaning it can only use characters: {allowed_in_unique_name}
|
|
74
|
+
"""
|
|
75
|
+
raise ValueError(msg)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NiceHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
79
|
+
def __add_whitespace(self, idx, amount, text):
|
|
80
|
+
if idx == 0:
|
|
81
|
+
return text
|
|
82
|
+
return (" " * amount) + text
|
|
83
|
+
|
|
84
|
+
def _split_lines(self, text, width):
|
|
85
|
+
textRows = text.splitlines()
|
|
86
|
+
for idx, line in enumerate(textRows):
|
|
87
|
+
search = re.search(r"\s*[0-9\-]{0,}\.?\s*", line)
|
|
88
|
+
if line.strip() == "":
|
|
89
|
+
textRows[idx] = " "
|
|
90
|
+
elif search:
|
|
91
|
+
whitespace_needed = search.end()
|
|
92
|
+
lines = [
|
|
93
|
+
self.__add_whitespace(i, whitespace_needed, x)
|
|
94
|
+
for i, x in enumerate(_textwrap.wrap(line, width))
|
|
95
|
+
]
|
|
96
|
+
textRows[idx] = lines
|
|
97
|
+
|
|
98
|
+
return [item for sublist in textRows for item in sublist]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ToolParser(argparse.ArgumentParser):
|
|
102
|
+
|
|
103
|
+
def error(self, message):
|
|
104
|
+
if message.startswith("unrecognized arguments"):
|
|
105
|
+
unrecognized = message.split(": ")[1]
|
|
106
|
+
if not unrecognized.startswith("-"):
|
|
107
|
+
# This was probably a misspelled tool name
|
|
108
|
+
message = message + (
|
|
109
|
+
f". If `{unrecognized}` was intended to invoke "
|
|
110
|
+
"a tool, please run `lemonade -h` and check the spelling and "
|
|
111
|
+
"availability of that tool."
|
|
112
|
+
)
|
|
113
|
+
self.print_usage()
|
|
114
|
+
printing.log_error(message)
|
|
115
|
+
self.exit(2)
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, short_description: str, description: str, prog: str, epilog: str, **kwargs
|
|
119
|
+
):
|
|
120
|
+
super().__init__(
|
|
121
|
+
description=description,
|
|
122
|
+
prog=prog,
|
|
123
|
+
epilog=epilog,
|
|
124
|
+
formatter_class=NiceHelpFormatter,
|
|
125
|
+
**kwargs,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.short_description = short_description
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Tool(abc.ABC):
|
|
132
|
+
|
|
133
|
+
unique_name: str
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def helpful_parser(cls, short_description: str, **kwargs):
|
|
137
|
+
epilog = (
|
|
138
|
+
f"`{cls.unique_name}` is a Tool. It is intended to be invoked as "
|
|
139
|
+
"part of a sequence of Tools, for example: `lemonade -i INPUTS tool-one "
|
|
140
|
+
"tool-two tool-three`. Tools communicate data to each other via State. "
|
|
141
|
+
"You can learn more at "
|
|
142
|
+
"https://github.com/lemonade-sdk/lemonade/blob/main/docs/README.md"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return ToolParser(
|
|
146
|
+
prog=f"lemonade {cls.unique_name}",
|
|
147
|
+
short_description=short_description,
|
|
148
|
+
description=cls.__doc__,
|
|
149
|
+
epilog=epilog,
|
|
150
|
+
**kwargs,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def status_line(self, successful, verbosity):
|
|
154
|
+
"""
|
|
155
|
+
Print a line of status information for this Tool into the monitor.
|
|
156
|
+
"""
|
|
157
|
+
if verbosity:
|
|
158
|
+
# Only use special characters when the terminal encoding supports it
|
|
159
|
+
if sys.stdout.encoding == "utf-8":
|
|
160
|
+
success_tick = "✓"
|
|
161
|
+
fail_tick = "×"
|
|
162
|
+
else:
|
|
163
|
+
success_tick = "+"
|
|
164
|
+
fail_tick = "x"
|
|
165
|
+
|
|
166
|
+
if self.percent_progress is None:
|
|
167
|
+
progress_indicator = ""
|
|
168
|
+
else:
|
|
169
|
+
progress_indicator = f" ({self.percent_progress:.1f}%)"
|
|
170
|
+
|
|
171
|
+
if successful is None:
|
|
172
|
+
# Initialize the message
|
|
173
|
+
printing.logn(f" {self.monitor_message} ")
|
|
174
|
+
elif successful:
|
|
175
|
+
# Print success message
|
|
176
|
+
printing.log(f" {success_tick} ", c=printing.Colors.OKGREEN)
|
|
177
|
+
printing.logn(
|
|
178
|
+
self.monitor_message + progress_indicator + " "
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# successful == False, print failure message
|
|
182
|
+
printing.log(f" {fail_tick} ", c=printing.Colors.FAIL)
|
|
183
|
+
printing.logn(
|
|
184
|
+
self.monitor_message + progress_indicator + " "
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
monitor_message,
|
|
190
|
+
enable_logger=True,
|
|
191
|
+
):
|
|
192
|
+
_name_is_file_safe(self.__class__.unique_name)
|
|
193
|
+
|
|
194
|
+
self.status_key = f"{fs.Keys.TOOL_STATUS}:{self.__class__.unique_name}"
|
|
195
|
+
self.duration_key = f"{fs.Keys.TOOL_DURATION}:{self.__class__.unique_name}"
|
|
196
|
+
self.memory_key = f"{fs.Keys.TOOL_MEMORY}:{self.__class__.unique_name}"
|
|
197
|
+
self.monitor_message = monitor_message
|
|
198
|
+
self.progress = None
|
|
199
|
+
self.progress_queue = None
|
|
200
|
+
self.percent_progress = None
|
|
201
|
+
self.logfile_path = None
|
|
202
|
+
# Tools can disable build.Logger, which captures all stdout and stderr from
|
|
203
|
+
# the Tool, by setting enable_logger=False
|
|
204
|
+
self.enable_logger = enable_logger
|
|
205
|
+
# Tools can provide a list of keys that can be found in
|
|
206
|
+
# evaluation stats. Those key:value pairs will be presented
|
|
207
|
+
# in the status at the end of the build.
|
|
208
|
+
self.status_stats = []
|
|
209
|
+
|
|
210
|
+
@abc.abstractmethod
|
|
211
|
+
def run(self, state: State) -> State:
|
|
212
|
+
"""
|
|
213
|
+
Execute the functionality of the Tool by acting on the state.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
@abc.abstractmethod
|
|
218
|
+
def parser() -> argparse.ArgumentParser:
|
|
219
|
+
"""
|
|
220
|
+
Static method that returns an ArgumentParser that defines the command
|
|
221
|
+
line interface for this Tool.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def set_percent_progress(self, percent_progress: float):
|
|
225
|
+
"""
|
|
226
|
+
Update the progress monitor with a percent progress to let the user
|
|
227
|
+
know how much progress the Tool has made.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if percent_progress is not None and not isinstance(percent_progress, float):
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Input argument must be a float or None, got {percent_progress}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if self.progress_queue:
|
|
236
|
+
self.progress_queue.put(percent_progress)
|
|
237
|
+
self.percent_progress = percent_progress
|
|
238
|
+
|
|
239
|
+
# pylint: disable=unused-argument
|
|
240
|
+
def parse(self, state: State, args, known_only=True) -> argparse.Namespace:
|
|
241
|
+
"""
|
|
242
|
+
Run the parser and return a Namespace of keyword arguments that the user
|
|
243
|
+
passed to the Tool via the command line.
|
|
244
|
+
|
|
245
|
+
Tools should extend this function only if they require specific parsing
|
|
246
|
+
logic, for example decoding the name of a data type into a data type class.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
state: the same state passed into the run method of the Tool, useful if
|
|
250
|
+
the parse decoding logic needs to take the state into account.
|
|
251
|
+
args: command line arguments passed from the CLI.
|
|
252
|
+
known_only: this argument allows the CLI framework to
|
|
253
|
+
incrementally parse complex commands.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
if known_only:
|
|
257
|
+
parsed_args = self.__class__.parser().parse_args(args)
|
|
258
|
+
else:
|
|
259
|
+
parsed_args, _ = self.__class__.parser().parse_known_args(args)
|
|
260
|
+
|
|
261
|
+
return parsed_args
|
|
262
|
+
|
|
263
|
+
def parse_and_run(
|
|
264
|
+
self,
|
|
265
|
+
state: State,
|
|
266
|
+
args,
|
|
267
|
+
monitor: bool = False,
|
|
268
|
+
known_only=True,
|
|
269
|
+
) -> Dict:
|
|
270
|
+
"""
|
|
271
|
+
Helper function to parse CLI arguments into the args expected
|
|
272
|
+
by run(), and then forward them into the run() method.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
parsed_args = self.parse(state, args, known_only)
|
|
276
|
+
return self.run_helper(state, monitor, **parsed_args.__dict__)
|
|
277
|
+
|
|
278
|
+
def run_helper(
|
|
279
|
+
self, state: State, monitor: bool = False, **kwargs
|
|
280
|
+
) -> Tuple[State, int]:
|
|
281
|
+
"""
|
|
282
|
+
Wraps the developer-defined .run() method with helper functionality.
|
|
283
|
+
Specifically:
|
|
284
|
+
- Provides a path to a log file
|
|
285
|
+
- Redirects the stdout of the tool to that log file
|
|
286
|
+
- Monitors the progress of the tool on the command line,
|
|
287
|
+
including in the event of an exception
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
# Set the build status to INCOMPLETE to indicate that a Tool
|
|
291
|
+
# started running. This allows us to test whether the Tool exited
|
|
292
|
+
# unexpectedly, before it was able to set ERROR
|
|
293
|
+
state.build_status = build.FunctionStatus.INCOMPLETE
|
|
294
|
+
|
|
295
|
+
self.logfile_path = os.path.join(
|
|
296
|
+
build.output_dir(state.cache_dir, state.build_name),
|
|
297
|
+
f"log_{self.unique_name}.txt",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if monitor:
|
|
301
|
+
self.progress_queue = Queue()
|
|
302
|
+
self.progress = Process(
|
|
303
|
+
target=_spinner, args=(self.monitor_message, self.progress_queue)
|
|
304
|
+
)
|
|
305
|
+
self.progress.start()
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# Execute the build tool
|
|
309
|
+
|
|
310
|
+
if self.enable_logger:
|
|
311
|
+
with build.Logger(self.monitor_message, self.logfile_path):
|
|
312
|
+
state = self.run(state, **kwargs)
|
|
313
|
+
else:
|
|
314
|
+
state = self.run(state, **kwargs)
|
|
315
|
+
|
|
316
|
+
except Exception: # pylint: disable=broad-except
|
|
317
|
+
self.status_line(
|
|
318
|
+
successful=False,
|
|
319
|
+
verbosity=monitor,
|
|
320
|
+
)
|
|
321
|
+
state.build_status = build.FunctionStatus.ERROR
|
|
322
|
+
raise
|
|
323
|
+
|
|
324
|
+
else:
|
|
325
|
+
self.status_line(successful=True, verbosity=monitor)
|
|
326
|
+
|
|
327
|
+
# Tools should not set build.FunctionStatus.SUCCESSFUL for the whole build,
|
|
328
|
+
# as that is reserved for Sequence.launch()
|
|
329
|
+
if state.build_status == build.FunctionStatus.SUCCESSFUL:
|
|
330
|
+
raise exp.ToolError(
|
|
331
|
+
"Lemonade Tools are not allowed to set "
|
|
332
|
+
"`state.build_status == build.FunctionStatus.SUCCESSFUL`, "
|
|
333
|
+
"however that has happened. If you are a plugin developer, "
|
|
334
|
+
"do not do this. If you are a user, please file an issue at "
|
|
335
|
+
"https://github.com/lemonade-sdk/lemonade/issues."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
finally:
|
|
339
|
+
if monitor:
|
|
340
|
+
self.progress.terminate()
|
|
341
|
+
|
|
342
|
+
return state
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class FirstTool(Tool):
|
|
346
|
+
"""
|
|
347
|
+
Provides extra features for Tools that are meant to be the first Tool
|
|
348
|
+
in the sequence.
|
|
349
|
+
|
|
350
|
+
Specifically:
|
|
351
|
+
- FirstTools should not have any expectations of State.result, since
|
|
352
|
+
they populate State with an initial result.
|
|
353
|
+
- All FirstTools implicitly take an `input` argument that points to
|
|
354
|
+
the input to that Tool, for example an ONNX file or PyTorch script.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def helpful_parser(cls, short_description: str, **kwargs):
|
|
359
|
+
parser = super().helpful_parser(short_description, **kwargs)
|
|
360
|
+
|
|
361
|
+
# Argument required for any tool that starts a sequence
|
|
362
|
+
parser.add_argument("--input", help=argparse.SUPPRESS)
|
|
363
|
+
|
|
364
|
+
return parser
|
|
365
|
+
|
|
366
|
+
@abc.abstractmethod
|
|
367
|
+
def run(self, state: State, input=None) -> State:
|
|
368
|
+
"""
|
|
369
|
+
The run() method of any FirstTool must accept the `input` argument
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
374
|
+
# Modifications Copyright (c) 2025 AMD
|
lemonade/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "7.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .install import main as installcli
|