speedy-utils 1.1.17__py3-none-any.whl → 1.1.19__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.
- llm_utils/__init__.py +9 -1
- llm_utils/chat_format/display.py +109 -14
- llm_utils/lm/__init__.py +12 -11
- llm_utils/lm/async_lm/async_llm_task.py +1 -10
- llm_utils/lm/async_lm/async_lm.py +13 -4
- llm_utils/lm/async_lm/async_lm_base.py +24 -14
- llm_utils/lm/base_prompt_builder.py +288 -0
- llm_utils/lm/llm_task.py +693 -0
- llm_utils/lm/lm.py +207 -0
- llm_utils/lm/lm_base.py +285 -0
- llm_utils/lm/openai_memoize.py +2 -2
- llm_utils/vector_cache/core.py +285 -89
- speedy_utils/__init__.py +2 -1
- speedy_utils/common/patcher.py +68 -0
- speedy_utils/common/utils_cache.py +6 -6
- speedy_utils/common/utils_io.py +238 -8
- speedy_utils/multi_worker/process.py +180 -192
- speedy_utils/multi_worker/thread.py +94 -2
- {speedy_utils-1.1.17.dist-info → speedy_utils-1.1.19.dist-info}/METADATA +36 -14
- {speedy_utils-1.1.17.dist-info → speedy_utils-1.1.19.dist-info}/RECORD +24 -19
- {speedy_utils-1.1.17.dist-info → speedy_utils-1.1.19.dist-info}/WHEEL +1 -1
- speedy_utils-1.1.19.dist-info/entry_points.txt +5 -0
- speedy_utils-1.1.17.dist-info/entry_points.txt +0 -6
llm_utils/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
from llm_utils.lm.openai_memoize import MOpenAI
|
|
2
|
+
from llm_utils.lm import LLMTask, AsyncLM, AsyncLLMTask
|
|
2
3
|
from llm_utils.vector_cache import VectorCache
|
|
4
|
+
from llm_utils.lm.lm_base import get_model_name
|
|
5
|
+
from llm_utils.lm.base_prompt_builder import BasePromptBuilder
|
|
6
|
+
|
|
7
|
+
LLM = LLMTask
|
|
3
8
|
|
|
4
9
|
from .chat_format import (
|
|
5
10
|
build_chatml_input,
|
|
@@ -12,7 +17,6 @@ from .chat_format import (
|
|
|
12
17
|
transform_messages,
|
|
13
18
|
transform_messages_to_chatml,
|
|
14
19
|
)
|
|
15
|
-
from .lm.async_lm import AsyncLLMTask, AsyncLM
|
|
16
20
|
|
|
17
21
|
__all__ = [
|
|
18
22
|
"transform_messages",
|
|
@@ -26,6 +30,10 @@ __all__ = [
|
|
|
26
30
|
"display_chat_messages_as_html",
|
|
27
31
|
"AsyncLM",
|
|
28
32
|
"AsyncLLMTask",
|
|
33
|
+
"LLMTask",
|
|
29
34
|
"MOpenAI",
|
|
35
|
+
"get_model_name",
|
|
30
36
|
"VectorCache",
|
|
37
|
+
"BasePromptBuilder",
|
|
38
|
+
"LLM"
|
|
31
39
|
]
|
llm_utils/chat_format/display.py
CHANGED
|
@@ -1,19 +1,93 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from difflib import SequenceMatcher
|
|
4
5
|
from typing import Any, Optional
|
|
5
6
|
|
|
6
7
|
from IPython.display import HTML, display
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
def _preprocess_as_json(content: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Preprocess content as JSON with proper formatting and syntax highlighting.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
# Try to parse and reformat JSON
|
|
16
|
+
parsed = json.loads(content)
|
|
17
|
+
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
18
|
+
except (json.JSONDecodeError, TypeError):
|
|
19
|
+
# If not valid JSON, return as-is
|
|
20
|
+
return content
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _preprocess_as_markdown(content: str) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Preprocess content as markdown with proper formatting.
|
|
26
|
+
"""
|
|
27
|
+
# Basic markdown preprocessing - convert common patterns
|
|
28
|
+
lines = content.split('\n')
|
|
29
|
+
processed_lines = []
|
|
30
|
+
|
|
31
|
+
for line in lines:
|
|
32
|
+
# Convert **bold** to span with bold styling
|
|
33
|
+
while '**' in line:
|
|
34
|
+
first_pos = line.find('**')
|
|
35
|
+
if first_pos != -1:
|
|
36
|
+
second_pos = line.find('**', first_pos + 2)
|
|
37
|
+
if second_pos != -1:
|
|
38
|
+
before = line[:first_pos]
|
|
39
|
+
bold_text = line[first_pos + 2:second_pos]
|
|
40
|
+
after = line[second_pos + 2:]
|
|
41
|
+
line = f'{before}<span style="font-weight: bold;">{bold_text}</span>{after}'
|
|
42
|
+
else:
|
|
43
|
+
break
|
|
44
|
+
else:
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
# Convert *italic* to span with italic styling
|
|
48
|
+
while '*' in line and line.count('*') >= 2:
|
|
49
|
+
first_pos = line.find('*')
|
|
50
|
+
if first_pos != -1:
|
|
51
|
+
second_pos = line.find('*', first_pos + 1)
|
|
52
|
+
if second_pos != -1:
|
|
53
|
+
before = line[:first_pos]
|
|
54
|
+
italic_text = line[first_pos + 1:second_pos]
|
|
55
|
+
after = line[second_pos + 1:]
|
|
56
|
+
line = f'{before}<span style="font-style: italic;">{italic_text}</span>{after}'
|
|
57
|
+
else:
|
|
58
|
+
break
|
|
59
|
+
else:
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
# Convert # headers to bold headers
|
|
63
|
+
if line.strip().startswith('#'):
|
|
64
|
+
level = len(line) - len(line.lstrip('#'))
|
|
65
|
+
header_text = line.lstrip('# ').strip()
|
|
66
|
+
line = f'<span style="font-weight: bold; font-size: 1.{min(4, level)}em;">{header_text}</span>'
|
|
67
|
+
|
|
68
|
+
processed_lines.append(line)
|
|
69
|
+
|
|
70
|
+
return '\n'.join(processed_lines)
|
|
71
|
+
|
|
72
|
+
|
|
9
73
|
def show_chat(
|
|
10
74
|
msgs: Any,
|
|
11
75
|
return_html: bool = False,
|
|
12
76
|
file: str = "/tmp/conversation.html",
|
|
13
77
|
theme: str = "default",
|
|
78
|
+
as_markdown: bool = False,
|
|
79
|
+
as_json: bool = False,
|
|
14
80
|
) -> Optional[str]:
|
|
15
81
|
"""
|
|
16
82
|
Display chat messages as HTML.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
msgs: Chat messages in various formats
|
|
86
|
+
return_html: If True, return HTML string instead of displaying
|
|
87
|
+
file: Path to save HTML file
|
|
88
|
+
theme: Color theme ('default', 'light', 'dark')
|
|
89
|
+
as_markdown: If True, preprocess content as markdown
|
|
90
|
+
as_json: If True, preprocess content as JSON
|
|
17
91
|
"""
|
|
18
92
|
if isinstance(msgs, dict) and "messages" in msgs:
|
|
19
93
|
msgs = msgs["messages"]
|
|
@@ -74,45 +148,66 @@ def show_chat(
|
|
|
74
148
|
name = tool_call["name"]
|
|
75
149
|
args = tool_call["arguments"]
|
|
76
150
|
content += f"Tool: {name}\nArguments: {args}"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
151
|
+
|
|
152
|
+
# Preprocess content based on format options
|
|
153
|
+
if as_json:
|
|
154
|
+
content = _preprocess_as_json(content)
|
|
155
|
+
elif as_markdown:
|
|
156
|
+
content = _preprocess_as_markdown(content)
|
|
157
|
+
|
|
158
|
+
# Handle HTML escaping differently for markdown vs regular content
|
|
159
|
+
if as_markdown:
|
|
160
|
+
# For markdown, preserve HTML tags but escape other characters carefully
|
|
161
|
+
content = content.replace("\n", "<br>")
|
|
162
|
+
content = content.replace("\t", " ")
|
|
163
|
+
content = content.replace(" ", " ")
|
|
164
|
+
# Don't escape < and > for markdown since we want to preserve our span tags
|
|
165
|
+
else:
|
|
166
|
+
# Regular escaping for non-markdown content
|
|
167
|
+
content = content.replace("\n", "<br>")
|
|
168
|
+
content = content.replace("\t", " ")
|
|
169
|
+
content = content.replace(" ", " ")
|
|
170
|
+
content = (
|
|
171
|
+
content.replace("<br>", "TEMP_BR")
|
|
172
|
+
.replace("<", "<")
|
|
173
|
+
.replace(">", ">")
|
|
174
|
+
.replace("TEMP_BR", "<br>")
|
|
175
|
+
)
|
|
86
176
|
if role in color_scheme:
|
|
87
177
|
background_color = color_scheme[role]["background"]
|
|
88
178
|
text_color = color_scheme[role]["text"]
|
|
89
179
|
else:
|
|
90
180
|
background_color = color_scheme["default"]["background"]
|
|
91
181
|
text_color = color_scheme["default"]["text"]
|
|
182
|
+
|
|
183
|
+
# Choose container based on whether we have markdown formatting
|
|
184
|
+
content_container = "div" if as_markdown else "pre"
|
|
185
|
+
container_style = 'style="white-space: pre-wrap;"' if as_markdown else ""
|
|
186
|
+
|
|
92
187
|
if role == "system":
|
|
93
188
|
conversation_html += (
|
|
94
189
|
f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
|
|
95
|
-
f'<strong>System:</strong><br><
|
|
190
|
+
f'<strong>System:</strong><br><{content_container} id="system-{i}" {container_style}>{content}</{content_container}></div>'
|
|
96
191
|
)
|
|
97
192
|
elif role == "user":
|
|
98
193
|
conversation_html += (
|
|
99
194
|
f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
|
|
100
|
-
f'<strong>User:</strong><br><
|
|
195
|
+
f'<strong>User:</strong><br><{content_container} id="user-{i}" {container_style}>{content}</{content_container}></div>'
|
|
101
196
|
)
|
|
102
197
|
elif role == "assistant":
|
|
103
198
|
conversation_html += (
|
|
104
199
|
f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
|
|
105
|
-
f'<strong>Assistant:</strong><br><
|
|
200
|
+
f'<strong>Assistant:</strong><br><{content_container} id="assistant-{i}" {container_style}>{content}</{content_container}></div>'
|
|
106
201
|
)
|
|
107
202
|
elif role == "function":
|
|
108
203
|
conversation_html += (
|
|
109
204
|
f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
|
|
110
|
-
f'<strong>Function:</strong><br><
|
|
205
|
+
f'<strong>Function:</strong><br><{content_container} id="function-{i}" {container_style}>{content}</{content_container}></div>'
|
|
111
206
|
)
|
|
112
207
|
else:
|
|
113
208
|
conversation_html += (
|
|
114
209
|
f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
|
|
115
|
-
f'<strong>{role}:</strong><br><
|
|
210
|
+
f'<strong>{role}:</strong><br><{content_container} id="{role}-{i}" {container_style}>{content}</{content_container}><br>'
|
|
116
211
|
f"<button onclick=\"copyContent('{role}-{i}')\">Copy</button></div>"
|
|
117
212
|
)
|
|
118
213
|
html: str = f"""
|
llm_utils/lm/__init__.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from .async_lm.async_lm import AsyncLM
|
|
2
|
+
from .async_lm.async_llm_task import AsyncLLMTask
|
|
3
|
+
from .lm_base import LMBase, get_model_name
|
|
4
|
+
from .llm_task import LLMTask
|
|
5
|
+
from .base_prompt_builder import BasePromptBuilder
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# "AsyncLLMTask",
|
|
12
|
-
# ]
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LMBase",
|
|
9
|
+
"LLMTask",
|
|
10
|
+
"AsyncLM",
|
|
11
|
+
"AsyncLLMTask",
|
|
12
|
+
"BasePromptBuilder",
|
|
13
|
+
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# type: ignore
|
|
1
2
|
"""
|
|
2
3
|
Async LLM Task module for handling language model interactions with structured input/output.
|
|
3
4
|
"""
|
|
@@ -33,8 +34,6 @@ class LMConfiguration:
|
|
|
33
34
|
model: Optional[str] = None
|
|
34
35
|
temperature: Optional[float] = None
|
|
35
36
|
max_tokens: Optional[int] = None
|
|
36
|
-
host: Optional[str] = None
|
|
37
|
-
port: Optional[Union[int, str]] = None
|
|
38
37
|
base_url: Optional[str] = None
|
|
39
38
|
api_key: Optional[str] = None
|
|
40
39
|
cache: Optional[bool] = True
|
|
@@ -53,8 +52,6 @@ class LMConfiguration:
|
|
|
53
52
|
"model": self.model,
|
|
54
53
|
"temperature": self.temperature,
|
|
55
54
|
"max_tokens": self.max_tokens,
|
|
56
|
-
"host": self.host,
|
|
57
|
-
"port": self.port,
|
|
58
55
|
"base_url": self.base_url,
|
|
59
56
|
"api_key": self.api_key,
|
|
60
57
|
"cache": self.cache,
|
|
@@ -90,8 +87,6 @@ class AsyncLLMTask(ABC, Generic[InputModelType, OutputModelType]):
|
|
|
90
87
|
DEFAULT_CACHE_DIR: Optional[pathlib.Path] = None
|
|
91
88
|
DEFAULT_TEMPERATURE: Optional[float] = None
|
|
92
89
|
DEFAULT_MAX_TOKENS: Optional[int] = None
|
|
93
|
-
DEFAULT_HOST: Optional[str] = None
|
|
94
|
-
DEFAULT_PORT: Optional[Union[int, str]] = None
|
|
95
90
|
DEFAULT_TOP_P: Optional[float] = None
|
|
96
91
|
DEFAULT_PRESENCE_PENALTY: Optional[float] = None
|
|
97
92
|
DEFAULT_TOP_K: Optional[int] = None
|
|
@@ -112,8 +107,6 @@ class AsyncLLMTask(ABC, Generic[InputModelType, OutputModelType]):
|
|
|
112
107
|
model: Optional[str] = None,
|
|
113
108
|
temperature: Optional[float] = None,
|
|
114
109
|
max_tokens: Optional[int] = None,
|
|
115
|
-
host: Optional[str] = None,
|
|
116
|
-
port: Optional[Union[int, str]] = None,
|
|
117
110
|
base_url: Optional[str] = None,
|
|
118
111
|
api_key: Optional[str] = None,
|
|
119
112
|
cache: Optional[bool] = None,
|
|
@@ -139,8 +132,6 @@ class AsyncLLMTask(ABC, Generic[InputModelType, OutputModelType]):
|
|
|
139
132
|
max_tokens=max_tokens
|
|
140
133
|
if max_tokens is not None
|
|
141
134
|
else self.DEFAULT_MAX_TOKENS,
|
|
142
|
-
host=host if host is not None else self.DEFAULT_HOST,
|
|
143
|
-
port=port if port is not None else self.DEFAULT_PORT,
|
|
144
135
|
base_url=base_url if base_url is not None else self.DEFAULT_BASE_URL,
|
|
145
136
|
api_key=api_key if api_key is not None else self.DEFAULT_API_KEY,
|
|
146
137
|
cache=cache if cache is not None else self.DEFAULT_CACHE,
|
|
@@ -27,7 +27,6 @@ from ._utils import (
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def jloads_safe(content: str) -> Any:
|
|
30
|
-
# if contain ```json, remove it
|
|
31
30
|
if "```json" in content:
|
|
32
31
|
content = content.split("```json")[1].strip().split("```")[0].strip()
|
|
33
32
|
try:
|
|
@@ -72,8 +71,6 @@ class AsyncLM(AsyncLMBase):
|
|
|
72
71
|
print(f"Using model: {model}")
|
|
73
72
|
|
|
74
73
|
super().__init__(
|
|
75
|
-
host=host,
|
|
76
|
-
port=port,
|
|
77
74
|
ports=ports,
|
|
78
75
|
base_url=base_url,
|
|
79
76
|
cache=cache,
|
|
@@ -231,6 +228,7 @@ class AsyncLM(AsyncLMBase):
|
|
|
231
228
|
def _extract_assistant_message(self, choice): # -> dict[str, str] | dict[str, Any]:
|
|
232
229
|
# TODO this current assume choice is a dict with "reasoning_content" and "content"
|
|
233
230
|
has_reasoning = False
|
|
231
|
+
reasoning_content = ""
|
|
234
232
|
if "reasoning_content" in choice and isinstance(
|
|
235
233
|
choice["reasoning_content"], str
|
|
236
234
|
):
|
|
@@ -249,7 +247,7 @@ class AsyncLM(AsyncLMBase):
|
|
|
249
247
|
|
|
250
248
|
return assistant_msg
|
|
251
249
|
|
|
252
|
-
async def
|
|
250
|
+
async def call_with_messages(
|
|
253
251
|
self,
|
|
254
252
|
prompt: Optional[str] = None,
|
|
255
253
|
messages: Optional[RawMsgs] = None,
|
|
@@ -295,6 +293,17 @@ class AsyncLM(AsyncLMBase):
|
|
|
295
293
|
msg_dump = dict(assistant_msg)
|
|
296
294
|
return msg_dump, full_messages
|
|
297
295
|
|
|
296
|
+
|
|
297
|
+
def call_sync(
|
|
298
|
+
self,
|
|
299
|
+
prompt: Optional[str] = None,
|
|
300
|
+
messages: Optional[RawMsgs] = None,
|
|
301
|
+
max_tokens: Optional[int] = None,
|
|
302
|
+
):
|
|
303
|
+
"""Synchronous wrapper around the async __call__ method."""
|
|
304
|
+
import asyncio
|
|
305
|
+
return asyncio.run(self.__call__(prompt=prompt, messages=messages, max_tokens=max_tokens))
|
|
306
|
+
|
|
298
307
|
async def parse(
|
|
299
308
|
self,
|
|
300
309
|
instruction,
|
|
@@ -40,32 +40,40 @@ class AsyncLMBase:
|
|
|
40
40
|
def __init__(
|
|
41
41
|
self,
|
|
42
42
|
*,
|
|
43
|
-
host: str = "localhost",
|
|
44
|
-
port: Optional[Union[int, str]] = None,
|
|
45
43
|
base_url: Optional[str] = None,
|
|
46
44
|
api_key: Optional[str] = None,
|
|
47
45
|
cache: bool = True,
|
|
48
46
|
ports: Optional[List[int]] = None,
|
|
49
47
|
) -> None:
|
|
50
|
-
self.
|
|
51
|
-
self._host = host
|
|
52
|
-
self.base_url = base_url or (f"http://{host}:{port}/v1" if port else None)
|
|
48
|
+
self.base_url = base_url
|
|
53
49
|
self.api_key = api_key or os.getenv("OPENAI_API_KEY", "abc")
|
|
54
50
|
self._cache = cache
|
|
55
51
|
self.ports = ports
|
|
56
|
-
self._init_port = port # <-- store the port provided at init
|
|
57
52
|
|
|
58
53
|
@property
|
|
59
54
|
def client(self) -> MAsyncOpenAI:
|
|
60
55
|
# if have multiple ports
|
|
61
|
-
if self.ports:
|
|
56
|
+
if self.ports and self.base_url:
|
|
62
57
|
import random
|
|
63
|
-
|
|
58
|
+
import re
|
|
59
|
+
|
|
64
60
|
port = random.choice(self.ports)
|
|
65
|
-
|
|
61
|
+
# Replace port in base_url if it exists
|
|
62
|
+
base_url_pattern = r'(https?://[^:/]+):?\d*(/.*)?'
|
|
63
|
+
match = re.match(base_url_pattern, self.base_url)
|
|
64
|
+
if match:
|
|
65
|
+
host_part = match.group(1)
|
|
66
|
+
path_part = match.group(2) or '/v1'
|
|
67
|
+
api_base = f"{host_part}:{port}{path_part}"
|
|
68
|
+
else:
|
|
69
|
+
api_base = self.base_url
|
|
66
70
|
logger.debug(f"Using port: {port}")
|
|
67
71
|
else:
|
|
68
|
-
api_base = self.base_url
|
|
72
|
+
api_base = self.base_url
|
|
73
|
+
|
|
74
|
+
if api_base is None:
|
|
75
|
+
raise ValueError("base_url must be provided")
|
|
76
|
+
|
|
69
77
|
client = MAsyncOpenAI(
|
|
70
78
|
api_key=self.api_key,
|
|
71
79
|
base_url=api_base,
|
|
@@ -182,11 +190,13 @@ class AsyncLMBase:
|
|
|
182
190
|
# ------------------------------------------------------------------ #
|
|
183
191
|
|
|
184
192
|
@staticmethod
|
|
185
|
-
async def list_models(
|
|
193
|
+
async def list_models(base_url: Optional[str] = None) -> List[str]:
|
|
186
194
|
try:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
if base_url is None:
|
|
196
|
+
raise ValueError("base_url must be provided")
|
|
197
|
+
client = AsyncLMBase(base_url=base_url).client
|
|
198
|
+
base_url_obj: URL = client.base_url
|
|
199
|
+
logger.debug(f"Base URL: {base_url_obj}")
|
|
190
200
|
models: AsyncSyncPage[Model] = await client.models.list() # type: ignore[assignment]
|
|
191
201
|
return [model.id for model in models.data]
|
|
192
202
|
except Exception as exc:
|