speedy-utils 1.1.47__py3-none-any.whl → 1.1.48__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 CHANGED
@@ -1,3 +1,4 @@
1
+ from llm_utils.llm_ray import LLMRay
1
2
  from llm_utils.lm import (
2
3
  LLM,
3
4
  AsyncLLMTask,
@@ -12,7 +13,6 @@ from llm_utils.lm import (
12
13
  from llm_utils.lm.base_prompt_builder import BasePromptBuilder
13
14
  from llm_utils.lm.lm_base import get_model_name
14
15
  from llm_utils.lm.openai_memoize import MOpenAI
15
- from llm_utils.llm_ray import LLMRay
16
16
  from llm_utils.vector_cache import VectorCache
17
17
 
18
18
 
@@ -37,7 +37,6 @@ from llm_utils.chat_format import (
37
37
  format_msgs,
38
38
  get_conversation_one_turn,
39
39
  show_chat,
40
- show_chat_v2,
41
40
  show_string_diff,
42
41
  transform_messages,
43
42
  transform_messages_to_chatml,
@@ -54,7 +53,6 @@ __all__ = [
54
53
  "build_chatml_input",
55
54
  "format_msgs",
56
55
  "display_chat_messages_as_html",
57
- "show_chat_v2",
58
56
  "AsyncLM",
59
57
  "AsyncLLMTask",
60
58
  "LLM",
@@ -4,7 +4,6 @@ from .display import (
4
4
  get_conversation_one_turn,
5
5
  highlight_diff_chars,
6
6
  show_chat,
7
- show_chat_v2,
8
7
  show_string_diff,
9
8
  )
10
9
  from .transform import (
@@ -32,5 +31,4 @@ __all__ = [
32
31
  "show_string_diff",
33
32
  "display_conversations",
34
33
  "display_chat_messages_as_html",
35
- "show_chat_v2",
36
34
  ]
@@ -2,248 +2,279 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from difflib import SequenceMatcher
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from IPython.display import HTML, display
8
8
 
9
9
 
10
10
  def _preprocess_as_json(content: str) -> str:
11
- """
12
- Preprocess content as JSON with proper formatting and syntax highlighting.
13
- """
11
+ """Preprocess content as JSON with proper formatting."""
14
12
  try:
15
- # Try to parse and reformat JSON
16
13
  parsed = json.loads(content)
17
14
  return json.dumps(parsed, indent=2, ensure_ascii=False)
18
15
  except (json.JSONDecodeError, TypeError):
19
- # If not valid JSON, return as-is
20
16
  return content
21
17
 
22
18
 
23
19
  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")
20
+ """Preprocess content as markdown with proper formatting."""
21
+ lines = content.split('\n')
29
22
  processed_lines = []
30
23
 
31
24
  for line in lines:
32
25
  # 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:
26
+ while '**' in line:
27
+ first_pos = line.find('**')
28
+ if first_pos == -1:
29
+ break
30
+ second_pos = line.find('**', first_pos + 2)
31
+ if second_pos == -1:
45
32
  break
33
+ before = line[:first_pos]
34
+ bold_text = line[first_pos + 2 : second_pos]
35
+ after = line[second_pos + 2 :]
36
+ line = f'{before}<span style="font-weight: bold;">{bold_text}</span>{after}'
46
37
 
47
38
  # 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:
39
+ while '*' in line and line.count('*') >= 2:
40
+ first_pos = line.find('*')
41
+ if first_pos == -1:
42
+ break
43
+ second_pos = line.find('*', first_pos + 1)
44
+ if second_pos == -1:
60
45
  break
46
+ before = line[:first_pos]
47
+ italic_text = line[first_pos + 1 : second_pos]
48
+ after = line[second_pos + 1 :]
49
+ line = (
50
+ f'{before}<span style="font-style: italic;">{italic_text}</span>{after}'
51
+ )
61
52
 
62
53
  # Convert # headers to bold headers
63
- if line.strip().startswith("#"):
64
- level = len(line) - len(line.lstrip("#"))
65
- header_text = line.lstrip("# ").strip()
54
+ if line.strip().startswith('#'):
55
+ level = len(line) - len(line.lstrip('#'))
56
+ header_text = line.lstrip('# ').strip()
66
57
  line = f'<span style="font-weight: bold; font-size: 1.{min(4, level)}em;">{header_text}</span>'
67
58
 
68
59
  processed_lines.append(line)
69
60
 
70
- return "\n".join(processed_lines)
61
+ return '\n'.join(processed_lines)
71
62
 
72
63
 
73
- def show_chat(
74
- msgs: Any,
75
- return_html: bool = False,
76
- file: str = "/tmp/conversation.html",
77
- theme: str = "default",
78
- as_markdown: bool = False,
79
- as_json: bool = False,
80
- ) -> str | None:
64
+ def _truncate_text(text: str, max_length: int, head_ratio: float = 0.3) -> str:
81
65
  """
82
- Display chat messages as HTML.
66
+ Truncate text if it exceeds max_length, showing head and tail with skip indicator.
83
67
 
84
68
  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
69
+ text: Text to truncate
70
+ max_length: Maximum length before truncation
71
+ head_ratio: Ratio of max_length to show at the head (default 0.3)
72
+
73
+ Returns:
74
+ Original text if within limit, otherwise truncated with [SKIP n chars] indicator
91
75
  """
92
- if isinstance(msgs, dict) and "messages" in msgs:
93
- msgs = msgs["messages"]
94
- assert isinstance(msgs, list) and all(
95
- isinstance(msg, dict) and "role" in msg and "content" in msg for msg in msgs
96
- ), "The input format is not recognized. Please specify the input format."
97
-
98
- if isinstance(msgs[-1], dict) and "choices" in msgs[-1]:
99
- message = msgs[-1]["choices"][0]["message"]
100
- reasoning_content = message.get("reasoning_content")
101
- content = message.get("content", "")
102
- if reasoning_content:
103
- content = reasoning_content + "\n" + content
104
- msgs[-1] = {
105
- "role": message["role"],
106
- "content": content,
107
- }
108
-
109
- themes: dict[str, dict[str, dict[str, str]]] = {
110
- "default": {
111
- "system": {"background": "#ffaaaa", "text": "#222222"}, # More red
112
- "user": {"background": "#f8c57e", "text": "#222222"}, # More orange
113
- "assistant": {"background": "#9dfebd", "text": "#222222"}, # More green
114
- "function": {"background": "#eafde7", "text": "#222222"},
115
- "tool": {"background": "#fde7fa", "text": "#222222"},
116
- "default": {"background": "#ffffff", "text": "#222222"},
117
- },
118
- "light": {
119
- "system": {"background": "#ff6666", "text": "#000000"}, # More red
120
- "user": {"background": "#ffd580", "text": "#000000"}, # More orange
121
- "assistant": {"background": "#80ffb3", "text": "#000000"}, # More green
122
- "function": {"background": "#AFFFFF", "text": "#000000"},
123
- "tool": {"background": "#FFAAFF", "text": "#000000"},
124
- "default": {"background": "#FFFFFF", "text": "#000000"},
125
- },
126
- "dark": {
127
- "system": {"background": "#b22222", "text": "#fffbe7"}, # More red
128
- "user": {"background": "#ff8800", "text": "#18181b"}, # More orange
129
- "assistant": {"background": "#22c55e", "text": "#e0ffe0"}, # More green
130
- "function": {"background": "#134e4a", "text": "#e0fff7"},
131
- "tool": {"background": "#701a75", "text": "#ffe0fa"},
132
- "default": {"background": "#18181b", "text": "#f4f4f5"},
133
- },
134
- }
135
-
136
- color_scheme = themes.get(theme, themes["default"])
137
-
138
- conversation_html = ""
139
- for i, message in enumerate(msgs):
140
- role = message["role"]
141
- content = message.get("content", "")
142
- if not content:
143
- content = ""
144
- tool_calls = message.get("tool_calls")
145
- if not content and tool_calls:
146
- for tool_call in tool_calls:
147
- tool_call = tool_call["function"]
148
- name = tool_call["name"]
149
- args = tool_call["arguments"]
150
- content += f"Tool: {name}\nArguments: {args}"
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", "&nbsp;&nbsp;&nbsp;&nbsp;")
163
- content = content.replace(" ", "&nbsp;&nbsp;")
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", "&nbsp;&nbsp;&nbsp;&nbsp;")
169
- content = content.replace(" ", "&nbsp;&nbsp;")
170
- content = (
171
- content.replace("<br>", "TEMP_BR")
172
- .replace("<", "&lt;")
173
- .replace(">", "&gt;")
174
- .replace("TEMP_BR", "<br>")
175
- )
176
- if role in color_scheme:
177
- background_color = color_scheme[role]["background"]
178
- text_color = color_scheme[role]["text"]
179
- else:
180
- background_color = color_scheme["default"]["background"]
181
- text_color = color_scheme["default"]["text"]
76
+ if len(text) <= max_length:
77
+ return text
78
+
79
+ head_len = int(max_length * head_ratio)
80
+ tail_len = max_length - head_len
81
+ skip_len = len(text) - head_len - tail_len
82
+
83
+ return f'{text[:head_len]}\n...[SKIP {skip_len} chars]...\n{text[-tail_len:]}'
84
+
85
+
86
+ def _format_reasoning_content(
87
+ reasoning: str, max_reasoning_length: int | None = None
88
+ ) -> str:
89
+ """
90
+ Format reasoning content with <think> tags.
91
+
92
+ Args:
93
+ reasoning: The reasoning content
94
+ max_reasoning_length: Max length before truncation (None = no truncation)
95
+
96
+ Returns:
97
+ Formatted reasoning with <think> tags
98
+ """
99
+ if max_reasoning_length is not None:
100
+ reasoning = _truncate_text(reasoning, max_reasoning_length)
101
+ return f'<think>\n{reasoning}\n</think>'
102
+
103
+
104
+ def _escape_html(content: str) -> str:
105
+ """Escape HTML special characters and convert whitespace for display."""
106
+ return (
107
+ content.replace('&', '&amp;')
108
+ .replace('<', '&lt;')
109
+ .replace('>', '&gt;')
110
+ .replace('\n', '<br>')
111
+ .replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;')
112
+ .replace(' ', '&nbsp;&nbsp;')
113
+ )
114
+
115
+
116
+ def _is_notebook() -> bool:
117
+ """Detect if running in a notebook environment."""
118
+ try:
119
+ from IPython.core.getipython import get_ipython
120
+
121
+ ipython = get_ipython()
122
+ return ipython is not None and 'IPKernelApp' in ipython.config
123
+ except (ImportError, AttributeError):
124
+ return False
125
+
126
+
127
+ # Color configurations
128
+ ROLE_COLORS_HTML = {
129
+ 'system': 'red',
130
+ 'user': 'darkorange',
131
+ 'assistant': 'green',
132
+ }
133
+
134
+ ROLE_COLORS_TERMINAL = {
135
+ 'system': '\033[91m', # Red
136
+ 'user': '\033[38;5;208m', # Orange
137
+ 'assistant': '\033[92m', # Green
138
+ }
139
+
140
+ ROLE_LABELS = {
141
+ 'system': 'System Instruction:',
142
+ 'user': 'User:',
143
+ 'assistant': 'Assistant:',
144
+ }
145
+
146
+ TERMINAL_RESET = '\033[0m'
147
+ TERMINAL_BOLD = '\033[1m'
148
+ TERMINAL_GRAY = '\033[90m'
149
+ TERMINAL_DIM = '\033[2m' # Dim text for reasoning
150
+
151
+ # HTML colors
152
+ HTML_REASONING_COLOR = '#AAAAAA' # Lighter gray for better readability
153
+
154
+
155
+ def _build_assistant_content_parts(
156
+ msg: dict[str, Any], max_reasoning_length: int | None
157
+ ) -> tuple[str | None, str]:
158
+ """
159
+ Build display content parts for assistant message.
160
+
161
+ Returns:
162
+ Tuple of (reasoning_formatted, answer_content)
163
+ reasoning_formatted is None if no reasoning present
164
+ """
165
+ content = msg.get('content', '')
166
+ reasoning = msg.get('reasoning_content')
167
+
168
+ if reasoning:
169
+ formatted_reasoning = _format_reasoning_content(reasoning, max_reasoning_length)
170
+ return formatted_reasoning, content
171
+
172
+ return None, content
182
173
 
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
174
 
187
- if role == "system":
188
- conversation_html += (
189
- f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
190
- f'<strong>System:</strong><br><{content_container} id="system-{i}" {container_style}>{content}</{content_container}></div>'
175
+ def _show_chat_html(
176
+ messages: list[dict[str, Any]], max_reasoning_length: int | None
177
+ ) -> None:
178
+ """Display chat messages as HTML in notebook."""
179
+ html_parts = [
180
+ "<div style='font-family:monospace; line-height:1.6em; white-space:pre-wrap;'>"
181
+ ]
182
+ separator = "<div style='color:#888; margin:0.5em 0;'>───────────────────────────────────────────────────</div>"
183
+
184
+ for i, msg in enumerate(messages):
185
+ role = msg.get('role', 'unknown').lower()
186
+ color = ROLE_COLORS_HTML.get(role, 'black')
187
+ label = ROLE_LABELS.get(role, f'{role.capitalize()}:')
188
+
189
+ if role == 'assistant':
190
+ reasoning, answer = _build_assistant_content_parts(
191
+ msg, max_reasoning_length
191
192
  )
192
- elif role == "user":
193
- conversation_html += (
194
- f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
195
- f'<strong>User:</strong><br><{content_container} id="user-{i}" {container_style}>{content}</{content_container}></div>'
193
+ html_parts.append(
194
+ f"<div><strong style='color:{color}'>{label}</strong><br>"
196
195
  )
197
- elif role == "assistant":
198
- conversation_html += (
199
- f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
200
- f'<strong>Assistant:</strong><br><{content_container} id="assistant-{i}" {container_style}>{content}</{content_container}></div>'
196
+ if reasoning:
197
+ escaped_reasoning = _escape_html(reasoning)
198
+ html_parts.append(
199
+ f"<span style='color:{HTML_REASONING_COLOR}'>{escaped_reasoning}</span><br><br>"
200
+ )
201
+ if answer:
202
+ escaped_answer = _escape_html(answer)
203
+ html_parts.append(
204
+ f"<span style='color:{color}'>{escaped_answer}</span>"
205
+ )
206
+ html_parts.append('</div>')
207
+ else:
208
+ content = msg.get('content', '')
209
+ escaped_content = _escape_html(content)
210
+ html_parts.append(
211
+ f"<div style='color:{color}'><strong>{label}</strong><br>{escaped_content}</div>"
201
212
  )
202
- elif role == "function":
203
- conversation_html += (
204
- f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
205
- f'<strong>Function:</strong><br><{content_container} id="function-{i}" {container_style}>{content}</{content_container}></div>'
213
+
214
+ if i < len(messages) - 1:
215
+ html_parts.append(separator)
216
+
217
+ html_parts.append('</div>')
218
+ display(HTML(''.join(html_parts)))
219
+
220
+
221
+ def _show_chat_terminal(
222
+ messages: list[dict[str, Any]], max_reasoning_length: int | None
223
+ ) -> None:
224
+ """Display chat messages with ANSI colors in terminal."""
225
+ separator = f'{TERMINAL_GRAY}─────────────────────────────────────────────────────────{TERMINAL_RESET}'
226
+
227
+ for i, msg in enumerate(messages):
228
+ role = msg.get('role', 'unknown').lower()
229
+ color = ROLE_COLORS_TERMINAL.get(role, '')
230
+ label = ROLE_LABELS.get(role, f'{role.capitalize()}:')
231
+
232
+ print(f'{color}{TERMINAL_BOLD}{label}{TERMINAL_RESET}')
233
+
234
+ if role == 'assistant':
235
+ reasoning, answer = _build_assistant_content_parts(
236
+ msg, max_reasoning_length
206
237
  )
238
+ if reasoning:
239
+ # Use lighter gray without dim for better readability
240
+ print(f'\033[38;5;246m{reasoning.strip()}{TERMINAL_RESET}')
241
+ if answer:
242
+ print() # Blank line between reasoning and answer
243
+ if answer:
244
+ print(f'{color}{answer.strip()}{TERMINAL_RESET}')
207
245
  else:
208
- conversation_html += (
209
- f'<div style="background-color: {background_color}; color: {text_color}; padding: 10px; margin-bottom: 10px;">'
210
- f'<strong>{role}:</strong><br><{content_container} id="{role}-{i}" {container_style}>{content}</{content_container}><br>'
211
- f"<button onclick=\"copyContent('{role}-{i}')\">Copy</button></div>"
212
- )
213
- html: str = f"""
214
- <html>
215
- <head>
216
- <style>
217
- pre {{
218
- white-space: pre-wrap;
219
- }}
220
- </style>
221
- </head>
222
- <body>
223
- {conversation_html}
224
- <script>
225
- function copyContent(elementId) {{
226
- var element = document.getElementById(elementId);
227
- var text = element.innerText;
228
- navigator.clipboard.writeText(text)
229
- .then(function() {{
230
- alert("Content copied to clipboard!");
231
- }})
232
- .catch(function(error) {{
233
- console.error("Error copying content: ", error);
234
- }});
235
- }}
236
- </script>
237
- </body>
238
- </html>
246
+ content = msg.get('content', '')
247
+ print(f'{color}{content}{TERMINAL_RESET}')
248
+
249
+ if i < len(messages) - 1:
250
+ print(separator)
251
+
252
+
253
+ def show_chat(
254
+ messages: list[dict[str, Any]], max_reasoning_length: int | None = 2000
255
+ ) -> None:
256
+ """
257
+ Display chat messages with colored formatting.
258
+
259
+ Automatically detects notebook vs terminal environment and formats accordingly.
260
+ Handles reasoning_content in assistant messages, formatting it with <think> tags.
261
+
262
+ Args:
263
+ messages: List of message dicts with 'role', 'content', and optionally 'reasoning_content'
264
+ max_reasoning_length: Max chars for reasoning before truncation (None = no limit)
265
+
266
+ Example:
267
+ >>> messages = [
268
+ ... {"role": "system", "content": "You are helpful."},
269
+ ... {"role": "user", "content": "Hello!"},
270
+ ... {"role": "assistant", "content": "Hi!", "reasoning_content": "User greeted me..."},
271
+ ... ]
272
+ >>> show_chat(messages)
239
273
  """
240
- if file:
241
- with open(file, "w") as f:
242
- f.write(html)
243
- if return_html:
244
- return html
245
- display(HTML(html))
246
- return None
274
+ if _is_notebook():
275
+ _show_chat_html(messages, max_reasoning_length)
276
+ else:
277
+ _show_chat_terminal(messages, max_reasoning_length)
247
278
 
248
279
 
249
280
  def get_conversation_one_turn(
@@ -251,204 +282,92 @@ def get_conversation_one_turn(
251
282
  user_msg: str | None = None,
252
283
  assistant_msg: str | None = None,
253
284
  assistant_prefix: str | None = None,
254
- return_format: str = "chatml",
285
+ return_format: str = 'chatml',
255
286
  ) -> Any:
256
- """
257
- Build a one-turn conversation.
258
- """
287
+ """Build a one-turn conversation."""
259
288
  messages: list[dict[str, str]] = []
289
+
260
290
  if system_msg is not None:
261
- messages.append({"role": "system", "content": system_msg})
291
+ messages.append({'role': 'system', 'content': system_msg})
262
292
  if user_msg is not None:
263
- messages.append({"role": "user", "content": user_msg})
293
+ messages.append({'role': 'user', 'content': user_msg})
264
294
  if assistant_msg is not None:
265
- messages.append({"role": "assistant", "content": assistant_msg})
295
+ messages.append({'role': 'assistant', 'content': assistant_msg})
296
+
266
297
  if assistant_prefix is not None:
267
- assert (
268
- return_format != "chatml"
269
- ), 'Change return_format to "text" if you want to use assistant_prefix'
270
- assert messages[-1]["role"] == "user"
298
+ if return_format == 'chatml':
299
+ raise ValueError('Change return_format to "text" to use assistant_prefix')
300
+ if not messages or messages[-1]['role'] != 'user':
301
+ raise ValueError(
302
+ 'Last message must be from user when using assistant_prefix'
303
+ )
304
+
271
305
  from .transform import transform_messages
272
306
 
273
- msg = transform_messages(messages, "chatml", "text", add_generation_prompt=True)
274
- if not isinstance(msg, str):
275
- msg = str(msg)
276
- msg += assistant_prefix
277
- return msg
278
- assert return_format in ["chatml"]
307
+ msg = transform_messages(messages, 'chatml', 'text', add_generation_prompt=True)
308
+ return str(msg) + assistant_prefix
309
+
310
+ if return_format != 'chatml':
311
+ raise ValueError(f'Unsupported return_format: {return_format}')
312
+
279
313
  return messages
280
314
 
281
315
 
282
316
  def highlight_diff_chars(text1: str, text2: str) -> str:
283
- """
284
- Return a string with deletions in red and additions in green.
285
- """
317
+ """Return a string with deletions in red and additions in green."""
286
318
  matcher = SequenceMatcher(None, text1, text2)
287
- html: list[str] = []
319
+ html_parts: list[str] = []
320
+
288
321
  for tag, i1, i2, j1, j2 in matcher.get_opcodes():
289
- if tag == "equal":
290
- html.append(text1[i1:i2])
291
- elif tag == "replace":
322
+ if tag == 'equal':
323
+ html_parts.append(text1[i1:i2])
324
+ elif tag == 'replace':
292
325
  if i1 != i2:
293
- html.append(
326
+ html_parts.append(
294
327
  f'<span style="background-color:#ffd6d6; color:#b20000;">{text1[i1:i2]}</span>'
295
328
  )
296
329
  if j1 != j2:
297
- html.append(
330
+ html_parts.append(
298
331
  f'<span style="background-color:#d6ffd6; color:#006600;">{text2[j1:j2]}</span>'
299
332
  )
300
- elif tag == "delete":
301
- html.append(
333
+ elif tag == 'delete':
334
+ html_parts.append(
302
335
  f'<span style="background-color:#ffd6d6; color:#b20000;">{text1[i1:i2]}</span>'
303
336
  )
304
- elif tag == "insert":
305
- html.append(
337
+ elif tag == 'insert':
338
+ html_parts.append(
306
339
  f'<span style="background-color:#d6ffd6; color:#006600;">{text2[j1:j2]}</span>'
307
340
  )
308
- return "".join(html)
309
-
310
-
311
- def show_string_diff(old: str, new: str) -> None:
312
- """
313
- Display a one-line visual diff between two strings (old -> new).
314
- """
315
- html1 = highlight_diff_chars(old, new)
316
- display(HTML(html1))
317
341
 
342
+ return ''.join(html_parts)
318
343
 
319
- def show_chat_v2(messages: list[dict[str, str]]):
320
- """
321
- Print only content of messages in different colors:
322
- system -> red, user -> orange, assistant -> green.
323
- Automatically detects notebook environment and uses appropriate display.
324
- """
325
- # Detect if running in a notebook environment
326
- try:
327
- from IPython.core.getipython import get_ipython
328
344
 
329
- ipython = get_ipython()
330
- is_notebook = ipython is not None and "IPKernelApp" in ipython.config
331
- except (ImportError, AttributeError):
332
- is_notebook = False
333
-
334
- if is_notebook:
335
- # Use HTML display in notebook
336
- from IPython.display import HTML, display
337
-
338
- role_colors = {
339
- "system": "red",
340
- "user": "darkorange",
341
- "assistant": "green",
342
- }
343
-
344
- role_labels = {
345
- "system": "System Instruction:",
346
- "user": "User:",
347
- "assistant": "Assistant:",
348
- }
349
-
350
- html = "<div style='font-family:monospace; line-height:1.6em; white-space:pre-wrap;'>"
351
- for i, msg in enumerate(messages):
352
- role = msg.get("role", "unknown").lower()
353
- content = msg.get("content", "")
354
- # Escape HTML characters
355
- content = (
356
- content.replace("&", "&amp;")
357
- .replace("<", "&lt;")
358
- .replace(">", "&gt;")
359
- .replace("\n", "<br>")
360
- .replace("\t", "&nbsp;&nbsp;&nbsp;&nbsp;")
361
- .replace(" ", "&nbsp;&nbsp;")
362
- )
363
- color = role_colors.get(role, "black")
364
- label = role_labels.get(role, f"{role.capitalize()}:")
365
- html += f"<div style='color:{color}'><strong>{label}</strong><br>{content}</div>"
366
- # Add separator except after last message
367
- if i < len(messages) - 1:
368
- html += "<div style='color:#888; margin:0.5em 0;'>───────────────────────────────────────────────────</div>"
369
- html += "</div>"
370
-
371
- display(HTML(html))
372
- else:
373
- # Use normal terminal printing with ANSI colors
374
- role_colors = {
375
- "system": "\033[91m", # Red
376
- "user": "\033[38;5;208m", # Orange
377
- "assistant": "\033[92m", # Green
378
- }
379
- reset = "\033[0m"
380
- separator_color = "\033[90m" # Gray
381
- bold = "\033[1m"
382
-
383
- role_labels = {
384
- "system": "System Instruction:",
385
- "user": "User:",
386
- "assistant": "Assistant:",
387
- }
388
-
389
- for i, msg in enumerate(messages):
390
- role = msg.get("role", "unknown").lower()
391
- content = msg.get("content", "")
392
- color = role_colors.get(role, "")
393
- label = role_labels.get(role, f"{role.capitalize()}:")
394
- print(f"{color}{bold}{label}{reset}")
395
- print(f"{color}{content}{reset}")
396
- # Add separator except after last message
397
- if i < len(messages) - 1:
398
- print(
399
- f"{separator_color}─────────────────────────────────────────────────────────{reset}"
400
- )
345
+ def show_string_diff(old: str, new: str) -> None:
346
+ """Display a visual diff between two strings (old -> new)."""
347
+ display(HTML(highlight_diff_chars(old, new)))
401
348
 
402
349
 
403
- def display_conversations(data1: Any, data2: Any, theme: str = "light") -> None:
404
- """
405
- Display two conversations side by side.
406
- """
350
+ def display_conversations(data1: Any, data2: Any) -> None:
351
+ """Display two conversations side by side. Deprecated."""
407
352
  import warnings
408
353
 
409
354
  warnings.warn(
410
- "display_conversations will be deprecated in the next version.",
355
+ 'display_conversations is deprecated and will be removed.',
411
356
  DeprecationWarning,
412
357
  stacklevel=2,
413
358
  )
414
- html1 = show_chat(data1, return_html=True, theme=theme)
415
- html2 = show_chat(data2, return_html=True, theme=theme)
416
- html = f"""
417
- <html>
418
- <head>
419
- <style>
420
- table {{
421
- width: 100%;
422
- border-collapse: collapse;
423
- }}
424
- td {{
425
- width: 50%;
426
- vertical-align: top;
427
- padding: 10px;
428
- }}
429
- </style>
430
- </head>
431
- <body>
432
- <table>
433
- <tr>
434
- <td>{html1}</td>
435
- <td>{html2}</td>
436
- </tr>
437
- </table>
438
- </body>
439
- </html>
440
- """
441
- display(HTML(html))
359
+ print('=== Conversation 1 ===')
360
+ show_chat(data1)
361
+ print('\n=== Conversation 2 ===')
362
+ show_chat(data2)
442
363
 
443
364
 
444
365
  def display_chat_messages_as_html(*args, **kwargs):
445
- """
446
- Use as show_chat and warn about the deprecated function.
447
- """
366
+ """Deprecated alias for show_chat."""
448
367
  import warnings
449
368
 
450
369
  warnings.warn(
451
- "display_chat_messages_as_html is deprecated, use show_chat instead.",
370
+ 'display_chat_messages_as_html is deprecated, use show_chat instead.',
452
371
  DeprecationWarning,
453
372
  stacklevel=2,
454
373
  )
@@ -456,10 +375,10 @@ def display_chat_messages_as_html(*args, **kwargs):
456
375
 
457
376
 
458
377
  __all__ = [
459
- "show_chat",
460
- "get_conversation_one_turn",
461
- "highlight_diff_chars",
462
- "show_string_diff",
463
- "display_conversations",
464
- "display_chat_messages_as_html",
378
+ 'show_chat',
379
+ 'get_conversation_one_turn',
380
+ 'highlight_diff_chars',
381
+ 'show_string_diff',
382
+ 'display_conversations',
383
+ 'display_chat_messages_as_html',
465
384
  ]
llm_utils/lm/llm.py CHANGED
@@ -9,12 +9,12 @@ from typing import Any, Dict, List, Optional, cast
9
9
 
10
10
  from httpx import Timeout
11
11
  from loguru import logger
12
- from openai import AuthenticationError, BadRequestError, OpenAI, RateLimitError
12
+ from openai import AuthenticationError, BadRequestError, OpenAI, RateLimitError, APITimeoutError
13
13
  from openai.types.chat import ChatCompletionMessageParam
14
14
  from pydantic import BaseModel
15
15
 
16
- from speedy_utils.common.utils_io import jdumps
17
16
  from speedy_utils import clean_traceback
17
+ from speedy_utils.common.utils_io import jdumps
18
18
 
19
19
  from .base_prompt_builder import BasePromptBuilder
20
20
  from .mixins import (
@@ -173,34 +173,45 @@ class LLM(
173
173
  )
174
174
  # Store raw response from client
175
175
  self.last_ai_response = completion
176
+ except APITimeoutError as exc:
177
+ error_msg = f'OpenAI API timeout ({api_kwargs['timeout']}) error: {exc} for model {model_name}'
178
+ logger.error(error_msg)
179
+ raise
176
180
  except (AuthenticationError, RateLimitError, BadRequestError) as exc:
177
181
  error_msg = f'OpenAI API error ({type(exc).__name__}): {exc}'
178
182
  logger.error(error_msg)
179
183
  raise
184
+ except ValueError as exc:
185
+ logger.error(f'ValueError during API call: {exc}')
186
+ raise
180
187
  except Exception as e:
181
188
  is_length_error = 'Length' in str(e) or 'maximum context length' in str(e)
182
189
  if is_length_error:
183
190
  raise ValueError(
184
191
  f'Input too long for model {model_name}. Error: {str(e)[:100]}...'
185
192
  ) from e
186
- # Re-raise all other exceptions
187
193
  raise
188
194
  # print(completion)
189
195
 
190
196
  results: list[dict[str, Any]] = []
191
197
  for choice in completion.choices:
198
+ assistant_message = [{'role': 'assistant', 'content': choice.message.content}]
199
+ try:
200
+ reasoning_content = choice.message.reasoning
201
+ except:
202
+ reasoning_content = None
203
+ if reasoning_content:
204
+ assistant_message[0]['reasoning_content'] = reasoning_content
205
+
192
206
  choice_messages = cast(
193
207
  Messages,
194
- messages + [{'role': 'assistant', 'content': choice.message.content}],
208
+ messages + assistant_message,
195
209
  )
196
210
  result_dict = {
197
211
  'parsed': choice.message.content,
198
212
  'messages': choice_messages,
199
213
  }
200
214
 
201
- # Add reasoning content if this is a reasoning model
202
- if self.is_reasoning_model and hasattr(choice.message, 'reasoning_content'):
203
- result_dict['reasoning_content'] = choice.message.reasoning_content
204
215
 
205
216
  results.append(result_dict)
206
217
  return results
@@ -394,12 +405,12 @@ class LLM(
394
405
  ) -> list[dict[str, Any]]:
395
406
  """Inspect the message history of a specific response choice."""
396
407
  if hasattr(self, '_last_conversations'):
397
- from llm_utils import show_chat_v2
408
+ from llm_utils import show_chat
398
409
 
399
410
  conv = self._last_conversations[idx]
400
411
  if k_last_messages > 0:
401
412
  conv = conv[-k_last_messages:]
402
- return show_chat_v2(conv)
413
+ return show_chat(conv)
403
414
  raise ValueError('No message history available. Make a call first.')
404
415
 
405
416
  def __inner_call__(
@@ -442,7 +453,7 @@ class LLM(
442
453
  is_reasoning_model: bool = False,
443
454
  lora_path: str | None = None,
444
455
  vllm_cmd: str | None = None,
445
- vllm_timeout: int = 120,
456
+ vllm_timeout: int = 0.1,
446
457
  vllm_reuse: bool = True,
447
458
  timeout: float | Timeout | None = None,
448
459
  **model_kwargs,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: speedy-utils
3
- Version: 1.1.47
3
+ Version: 1.1.48
4
4
  Summary: Fast and easy-to-use package for data science
5
5
  Project-URL: Homepage, https://github.com/anhvth/speedy
6
6
  Project-URL: Repository, https://github.com/anhvth/speedy
@@ -1,13 +1,13 @@
1
- llm_utils/__init__.py,sha256=_NUOQLYe67KQEsxIieePJfU6u9eb7_BS8RnuW31l7mE,1685
1
+ llm_utils/__init__.py,sha256=59W1jw-bNV2S3MWrCJihSnGM5fzcoPxh09MzZ2nM3Jo,1647
2
2
  llm_utils/group_messages.py,sha256=_XuFkEkO_iQDjVVx80XNFpufGH6iIDwHPDoCRJa60Ak,3691
3
3
  llm_utils/llm_ray.py,sha256=lnSq3eLyjfnODbIDgAzxrGqddqK-yUdOuHaDp6aMLZQ,12490
4
- llm_utils/chat_format/__init__.py,sha256=a7BKtBVktgLMq2Do4iNu3YfdDdTG1v9M_BkmaEorp5c,775
5
- llm_utils/chat_format/display.py,sha256=Lffjzna9_vV3QgfiXZM2_tuVb3wqA-WxwrmoAjsJigw,17356
4
+ llm_utils/chat_format/__init__.py,sha256=zfgtrMPzDHTTubWHzvbUSgODwyRHDsS2x2GSdrvr-78,737
5
+ llm_utils/chat_format/display.py,sha256=xeF3fH1h_OdEvaxM9Bl3PZsvyphxXblwB2lkZI_JM78,12806
6
6
  llm_utils/chat_format/transform.py,sha256=PJ2g9KT1GSbWuAs7giEbTpTAffpU9QsIXyRlbfpTZUQ,5351
7
7
  llm_utils/chat_format/utils.py,sha256=M2EctZ6NeHXqFYufh26Y3CpSphN0bdZm5xoNaEJj5vg,1251
8
8
  llm_utils/lm/__init__.py,sha256=4jYMy3wPH3tg-tHFyWEWOqrnmX4Tu32VZCdzRGMGQsI,778
9
9
  llm_utils/lm/base_prompt_builder.py,sha256=_TzYMsWr-SsbA_JNXptUVN56lV5RfgWWTrFi-E8LMy4,12337
10
- llm_utils/lm/llm.py,sha256=KEtrHq5D8ZkeD4iTc_zPgOtgVXpsCRA-A3fmWgYCz0w,21378
10
+ llm_utils/lm/llm.py,sha256=Qjfqd_MNPWblmVXglk-S8QpXFocBTMrG_D3YSA8y1r8,21725
11
11
  llm_utils/lm/llm_signature.py,sha256=vV8uZgLLd6ZKqWbq0OPywWvXAfl7hrJQnbtBF-VnZRU,1244
12
12
  llm_utils/lm/lm_base.py,sha256=Bk3q34KrcCK_bC4Ryxbc3KqkiPL39zuVZaBQ1i6wJqs,9437
13
13
  llm_utils/lm/mixins.py,sha256=Nz7CwJFBOvbZNbODUlJC04Pcbac3zWnT8vy7sZG_MVI,24906
@@ -61,7 +61,7 @@ vision_utils/README.md,sha256=AIDZZj8jo_QNrEjFyHwd00iOO431s-js-M2dLtVTn3I,5740
61
61
  vision_utils/__init__.py,sha256=hF54sT6FAxby8kDVhOvruy4yot8O-Ateey5n96O1pQM,284
62
62
  vision_utils/io_utils.py,sha256=pI0Va6miesBysJcllK6NXCay8HpGZsaMWwlsKB2DMgA,26510
63
63
  vision_utils/plot.py,sha256=HkNj3osA3moPuupP1VguXfPPOW614dZO5tvC-EFKpKM,12028
64
- speedy_utils-1.1.47.dist-info/METADATA,sha256=Ol3PRn5VGCiozaLocWrOLiYdO6nTHoEmBYhWODVfIi4,13073
65
- speedy_utils-1.1.47.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
66
- speedy_utils-1.1.47.dist-info/entry_points.txt,sha256=QY_2Vn6IcPCaqlY74pDRyZ6UTvPilaNPT7Gxijj7XI8,343
67
- speedy_utils-1.1.47.dist-info/RECORD,,
64
+ speedy_utils-1.1.48.dist-info/METADATA,sha256=bljryqMM922HaZoYDF3ZDuNCMXK9TeB-AvsU2jeqy-c,13073
65
+ speedy_utils-1.1.48.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
66
+ speedy_utils-1.1.48.dist-info/entry_points.txt,sha256=QY_2Vn6IcPCaqlY74pDRyZ6UTvPilaNPT7Gxijj7XI8,343
67
+ speedy_utils-1.1.48.dist-info/RECORD,,