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.

Files changed (61) hide show
  1. lemonade/__init__.py +5 -0
  2. lemonade/api.py +125 -0
  3. lemonade/cache.py +85 -0
  4. lemonade/cli.py +135 -0
  5. lemonade/common/__init__.py +0 -0
  6. lemonade/common/analyze_model.py +26 -0
  7. lemonade/common/build.py +223 -0
  8. lemonade/common/cli_helpers.py +139 -0
  9. lemonade/common/exceptions.py +98 -0
  10. lemonade/common/filesystem.py +368 -0
  11. lemonade/common/labels.py +61 -0
  12. lemonade/common/onnx_helpers.py +176 -0
  13. lemonade/common/plugins.py +10 -0
  14. lemonade/common/printing.py +110 -0
  15. lemonade/common/status.py +490 -0
  16. lemonade/common/system_info.py +390 -0
  17. lemonade/common/tensor_helpers.py +83 -0
  18. lemonade/common/test_helpers.py +28 -0
  19. lemonade/profilers/__init__.py +1 -0
  20. lemonade/profilers/memory_tracker.py +257 -0
  21. lemonade/profilers/profiler.py +55 -0
  22. lemonade/sequence.py +363 -0
  23. lemonade/state.py +159 -0
  24. lemonade/tools/__init__.py +1 -0
  25. lemonade/tools/adapter.py +104 -0
  26. lemonade/tools/bench.py +284 -0
  27. lemonade/tools/huggingface_bench.py +267 -0
  28. lemonade/tools/huggingface_load.py +520 -0
  29. lemonade/tools/humaneval.py +258 -0
  30. lemonade/tools/llamacpp.py +261 -0
  31. lemonade/tools/llamacpp_bench.py +154 -0
  32. lemonade/tools/management_tools.py +273 -0
  33. lemonade/tools/mmlu.py +327 -0
  34. lemonade/tools/ort_genai/__init__.py +0 -0
  35. lemonade/tools/ort_genai/oga.py +1129 -0
  36. lemonade/tools/ort_genai/oga_bench.py +142 -0
  37. lemonade/tools/perplexity.py +146 -0
  38. lemonade/tools/prompt.py +228 -0
  39. lemonade/tools/quark/__init__.py +0 -0
  40. lemonade/tools/quark/quark_load.py +172 -0
  41. lemonade/tools/quark/quark_quantize.py +439 -0
  42. lemonade/tools/report/__init__.py +0 -0
  43. lemonade/tools/report/llm_report.py +203 -0
  44. lemonade/tools/report/table.py +739 -0
  45. lemonade/tools/server/__init__.py +0 -0
  46. lemonade/tools/server/serve.py +1354 -0
  47. lemonade/tools/server/tool_calls.py +146 -0
  48. lemonade/tools/tool.py +374 -0
  49. lemonade/version.py +1 -0
  50. lemonade_install/__init__.py +1 -0
  51. lemonade_install/install.py +774 -0
  52. lemonade_sdk-7.0.0.dist-info/METADATA +116 -0
  53. lemonade_sdk-7.0.0.dist-info/RECORD +61 -0
  54. lemonade_sdk-7.0.0.dist-info/WHEEL +5 -0
  55. lemonade_sdk-7.0.0.dist-info/entry_points.txt +4 -0
  56. lemonade_sdk-7.0.0.dist-info/licenses/LICENSE +201 -0
  57. lemonade_sdk-7.0.0.dist-info/licenses/NOTICE.md +21 -0
  58. lemonade_sdk-7.0.0.dist-info/top_level.txt +3 -0
  59. lemonade_server/cli.py +260 -0
  60. lemonade_server/model_manager.py +98 -0
  61. 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