langroid 0.56.18__py3-none-any.whl → 0.57.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.
@@ -1565,7 +1565,7 @@ class ChatAgent(Agent):
1565
1565
  - CHAT_HISTORY_BUFFER
1566
1566
  )
1567
1567
  if output_len > self.config.llm.min_output_tokens:
1568
- logger.warning(
1568
+ logger.debug(
1569
1569
  f"""
1570
1570
  Chat Model context length is {self.llm.chat_context_length()},
1571
1571
  but the current message history is {self.chat_num_tokens(hist)}
@@ -217,25 +217,29 @@ class ChatDocument(Document):
217
217
  """
218
218
  tool_type = "" # FUNC or TOOL
219
219
  tool = "" # tool name or function name
220
- oai_tools = (
221
- []
222
- if self.oai_tool_calls is None
223
- else [t for t in self.oai_tool_calls if t.function is not None]
224
- )
225
- if self.function_call is not None:
226
- tool_type = "FUNC"
227
- tool = self.function_call.name
228
- elif len(oai_tools) > 0:
229
- tool_type = "OAI_TOOL"
230
- tool = ",".join(t.function.name for t in oai_tools) # type: ignore
231
- else:
232
- try:
233
- json_tools = self.get_tool_names()
234
- except Exception:
235
- json_tools = []
236
- if json_tools != []:
237
- tool_type = "TOOL"
238
- tool = json_tools[0]
220
+
221
+ # Skip tool detection for system messages - they contain tool instructions,
222
+ # not actual tool calls
223
+ if self.metadata.sender != Entity.SYSTEM:
224
+ oai_tools = (
225
+ []
226
+ if self.oai_tool_calls is None
227
+ else [t for t in self.oai_tool_calls if t.function is not None]
228
+ )
229
+ if self.function_call is not None:
230
+ tool_type = "FUNC"
231
+ tool = self.function_call.name
232
+ elif len(oai_tools) > 0:
233
+ tool_type = "OAI_TOOL"
234
+ tool = ",".join(t.function.name for t in oai_tools) # type: ignore
235
+ else:
236
+ try:
237
+ json_tools = self.get_tool_names()
238
+ except Exception:
239
+ json_tools = []
240
+ if json_tools != []:
241
+ tool_type = "TOOL"
242
+ tool = json_tools[0]
239
243
  recipient = self.metadata.recipient
240
244
  content = self.content
241
245
  sender_entity = self.metadata.sender
@@ -340,6 +344,50 @@ class ChatDocument(Document):
340
344
  ),
341
345
  )
342
346
 
347
+ @staticmethod
348
+ def from_LLMMessage(
349
+ message: LLMMessage,
350
+ sender_name: str = "",
351
+ recipient: str = "",
352
+ ) -> "ChatDocument":
353
+ """
354
+ Convert LLMMessage to ChatDocument.
355
+
356
+ Args:
357
+ message (LLMMessage): LLMMessage to convert.
358
+ sender_name (str): Name of the sender. Defaults to "".
359
+ recipient (str): Name of the recipient. Defaults to "".
360
+
361
+ Returns:
362
+ ChatDocument: ChatDocument representation of this LLMMessage.
363
+ """
364
+ # Map LLMMessage Role to ChatDocument Entity
365
+ role_to_entity = {
366
+ Role.USER: Entity.USER,
367
+ Role.SYSTEM: Entity.SYSTEM,
368
+ Role.ASSISTANT: Entity.LLM,
369
+ Role.FUNCTION: Entity.LLM,
370
+ Role.TOOL: Entity.LLM,
371
+ }
372
+
373
+ sender_entity = role_to_entity.get(message.role, Entity.USER)
374
+
375
+ return ChatDocument(
376
+ content=message.content or "",
377
+ content_any=message.content,
378
+ files=message.files,
379
+ function_call=message.function_call,
380
+ oai_tool_calls=message.tool_calls,
381
+ metadata=ChatDocMetaData(
382
+ source=sender_entity,
383
+ sender=sender_entity,
384
+ sender_name=sender_name,
385
+ recipient=recipient,
386
+ oai_tool_id=message.tool_call_id,
387
+ tool_ids=[message.tool_id] if message.tool_id else [],
388
+ ),
389
+ )
390
+
343
391
  @staticmethod
344
392
  def to_LLMMessage(
345
393
  message: Union[str, "ChatDocument"],
langroid/agent/task.py CHANGED
@@ -55,6 +55,7 @@ from langroid.utils.constants import (
55
55
  SEND_TO,
56
56
  USER_QUIT_STRINGS,
57
57
  )
58
+ from langroid.utils.html_logger import HTMLLogger
58
59
  from langroid.utils.logging import RichFileLogger, setup_file_logger
59
60
  from langroid.utils.object_registry import scheduled_cleanup
60
61
  from langroid.utils.system import hash
@@ -154,6 +155,7 @@ class TaskConfig(BaseModel):
154
155
  restart_as_subtask: bool = False
155
156
  logs_dir: str = "logs"
156
157
  enable_loggers: bool = True
158
+ enable_html_logging: bool = True
157
159
  addressing_prefix: str = ""
158
160
  allow_subtask_multi_oai_tools: bool = True
159
161
  recognize_string_signals: bool = True
@@ -343,6 +345,7 @@ class Task:
343
345
  self.session_id: str = ""
344
346
  self.logger: None | RichFileLogger = None
345
347
  self.tsv_logger: None | logging.Logger = None
348
+ self.html_logger: Optional[HTMLLogger] = None
346
349
  self.color_log: bool = False if settings.notebook else True
347
350
 
348
351
  self.n_stalled_steps = 0 # how many consecutive steps with no progress?
@@ -637,7 +640,20 @@ class Task:
637
640
 
638
641
  self._show_pending_message_if_debug()
639
642
  self.init_loggers()
640
- self.log_message(Entity.USER, self.pending_message)
643
+ # Log system message if it exists
644
+ if (
645
+ hasattr(self.agent, "_create_system_and_tools_message")
646
+ and hasattr(self.agent, "system_message")
647
+ and self.agent.system_message
648
+ ):
649
+ system_msg = self.agent._create_system_and_tools_message()
650
+ system_message_chat_doc = ChatDocument.from_LLMMessage(
651
+ system_msg,
652
+ sender_name=self.name or "system",
653
+ )
654
+ # log the system message
655
+ self.log_message(Entity.SYSTEM, system_message_chat_doc, mark=True)
656
+ self.log_message(Entity.USER, self.pending_message, mark=True)
641
657
  return self.pending_message
642
658
 
643
659
  def init_loggers(self) -> None:
@@ -667,6 +683,34 @@ class Task:
667
683
  header = ChatDocLoggerFields().tsv_header()
668
684
  self.tsv_logger.info(f" \tTask\tResponder\t{header}")
669
685
 
686
+ # HTML logger
687
+ if self.config.enable_html_logging:
688
+ if (
689
+ self.caller is not None
690
+ and hasattr(self.caller, "html_logger")
691
+ and self.caller.html_logger is not None
692
+ ):
693
+ self.html_logger = self.caller.html_logger
694
+ elif not hasattr(self, "html_logger") or self.html_logger is None:
695
+ from langroid.utils.html_logger import HTMLLogger
696
+
697
+ model_info = ""
698
+ if (
699
+ hasattr(self, "agent")
700
+ and hasattr(self.agent, "config")
701
+ and hasattr(self.agent.config, "llm")
702
+ ):
703
+ model_info = getattr(self.agent.config.llm, "chat_model", "")
704
+ self.html_logger = HTMLLogger(
705
+ filename=self.name,
706
+ log_dir=self.config.logs_dir,
707
+ model_info=model_info,
708
+ append=False,
709
+ )
710
+ # Log clickable file:// link to the HTML log
711
+ html_log_path = self.html_logger.file_path.resolve()
712
+ logger.warning(f"📊 HTML Log: file://{html_log_path}")
713
+
670
714
  def reset_all_sub_tasks(self) -> None:
671
715
  """
672
716
  Recursively reset message history & state of own agent and
@@ -2037,6 +2081,8 @@ class Task:
2037
2081
  mark (bool, optional): Whether to mark the message as the final result of
2038
2082
  a `task.step()` call. Defaults to False.
2039
2083
  """
2084
+ from langroid.agent.chat_document import ChatDocLoggerFields
2085
+
2040
2086
  default_values = ChatDocLoggerFields().dict().values()
2041
2087
  msg_str_tsv = "\t".join(str(v) for v in default_values)
2042
2088
  if msg is not None:
@@ -2077,6 +2123,48 @@ class Task:
2077
2123
  resp_str = str(resp)
2078
2124
  self.tsv_logger.info(f"{mark_str}\t{task_name}\t{resp_str}\t{msg_str_tsv}")
2079
2125
 
2126
+ # HTML logger
2127
+ if self.html_logger is not None:
2128
+ if msg is None:
2129
+ # Create a minimal fields object for None messages
2130
+ from langroid.agent.chat_document import ChatDocLoggerFields
2131
+
2132
+ fields_dict = {
2133
+ "responder": str(resp),
2134
+ "mark": "*" if mark else "",
2135
+ "task_name": self.name or "root",
2136
+ "content": "",
2137
+ "sender_entity": str(resp),
2138
+ "sender_name": "",
2139
+ "recipient": "",
2140
+ "block": None,
2141
+ "tool_type": "",
2142
+ "tool": "",
2143
+ }
2144
+ else:
2145
+ # Get fields from the message
2146
+ fields = msg.log_fields()
2147
+ fields_dict = fields.dict()
2148
+ fields_dict.update(
2149
+ {
2150
+ "responder": str(resp),
2151
+ "mark": "*" if mark else "",
2152
+ "task_name": self.name or "root",
2153
+ }
2154
+ )
2155
+
2156
+ # Create a ChatDocLoggerFields-like object for the HTML logger
2157
+ # Create a simple BaseModel subclass dynamically
2158
+ from langroid.pydantic_v1 import BaseModel
2159
+
2160
+ class LogFields(BaseModel):
2161
+ class Config:
2162
+ extra = "allow" # Allow extra fields
2163
+
2164
+ # Create instance with the fields from fields_dict
2165
+ log_obj = LogFields(**fields_dict)
2166
+ self.html_logger.log(log_obj)
2167
+
2080
2168
  def _valid_recipient(self, recipient: str) -> bool:
2081
2169
  """
2082
2170
  Is the recipient among the list of responders?
@@ -2335,6 +2423,13 @@ class Task:
2335
2423
  # Check if we matched the entire sequence
2336
2424
  return seq_idx == len(sequence.events)
2337
2425
 
2426
+ def close_loggers(self) -> None:
2427
+ """Close all loggers to ensure clean shutdown."""
2428
+ if hasattr(self, "logger") and self.logger is not None:
2429
+ self.logger.close()
2430
+ if hasattr(self, "html_logger") and self.html_logger is not None:
2431
+ self.html_logger.close()
2432
+
2338
2433
  def _matches_sequence_with_current(
2339
2434
  self,
2340
2435
  msg_chain: List[ChatDocument],
@@ -1526,7 +1526,7 @@ class OpenAIGPT(LanguageModel):
1526
1526
  usage = response.get("usage")
1527
1527
  if not cached and not self.get_stream() and usage is not None:
1528
1528
  prompt_tokens = usage.get("prompt_tokens") or 0
1529
- prompt_tokens_details = usage.get("prompt_tokens_details", {})
1529
+ prompt_tokens_details = usage.get("prompt_tokens_details", {}) or {}
1530
1530
  cached_tokens = prompt_tokens_details.get("cached_tokens") or 0
1531
1531
  completion_tokens = usage.get("completion_tokens") or 0
1532
1532
  cost = self._cost_chat_model(
@@ -0,0 +1,825 @@
1
+ """HTML Logger for Langroid Task System.
2
+
3
+ This module provides an HTML logger that creates self-contained HTML files
4
+ with collapsible log entries for better visualization of agent interactions.
5
+ """
6
+
7
+ import html
8
+ import json
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List
12
+
13
+ from langroid.pydantic_v1 import BaseModel
14
+ from langroid.utils.logging import setup_logger
15
+
16
+
17
+ class HTMLLogger:
18
+ """Logger that outputs task logs as interactive HTML files."""
19
+
20
+ def __init__(
21
+ self,
22
+ filename: str,
23
+ log_dir: str = "logs",
24
+ model_info: str = "",
25
+ append: bool = False,
26
+ ):
27
+ """Initialize the HTML logger.
28
+
29
+ Args:
30
+ filename: Base name for the log file (without extension)
31
+ log_dir: Directory to store log files
32
+ model_info: Information about the model being used
33
+ append: Whether to append to existing file
34
+ """
35
+ self.filename = filename
36
+ self.log_dir = Path(log_dir)
37
+ self.log_dir.mkdir(parents=True, exist_ok=True)
38
+ self.file_path = self.log_dir / f"{filename}.html"
39
+ self.model_info = model_info
40
+ self.entries: List[Dict[str, Any]] = []
41
+ self.entry_counter = 0
42
+ self.tool_counter = 0
43
+
44
+ # Logger for errors
45
+ self.logger = setup_logger(__name__)
46
+
47
+ if not append or not self.file_path.exists():
48
+ self._write_header()
49
+
50
+ def _write_header(self) -> None:
51
+ """Write the HTML header with CSS and JavaScript."""
52
+ timestamp = datetime.now().strftime("%m/%d/%Y, %I:%M:%S %p")
53
+
54
+ html_content = f"""<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="UTF-8">
58
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
59
+ <meta http-equiv="refresh" content="2">
60
+ <title>{self.filename} - Langroid Task Log</title>
61
+ <style>
62
+ body {{
63
+ background-color: #1e1e1e;
64
+ color: #f0f0f0;
65
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
66
+ font-size: 14px;
67
+ margin: 0;
68
+ padding: 20px;
69
+ line-height: 1.6;
70
+ }}
71
+
72
+ .header {{
73
+ border: 2px solid #d4a017;
74
+ padding: 15px;
75
+ margin-bottom: 20px;
76
+ color: #d4a017;
77
+ background-color: #2b2b2b;
78
+ border-radius: 5px;
79
+ }}
80
+
81
+ .header-line {{
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ }}
86
+
87
+ .separator {{
88
+ border-bottom: 2px solid #d4a017;
89
+ margin: 20px 0;
90
+ }}
91
+
92
+ .controls {{
93
+ margin-bottom: 20px;
94
+ }}
95
+
96
+ .controls {{
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 20px;
100
+ }}
101
+
102
+ .controls button {{
103
+ background-color: #333;
104
+ color: #f0f0f0;
105
+ border: 1px solid #555;
106
+ padding: 8px 16px;
107
+ cursor: pointer;
108
+ border-radius: 3px;
109
+ font-family: inherit;
110
+ }}
111
+
112
+ .controls button:hover {{
113
+ background-color: #444;
114
+ border-color: #d4a017;
115
+ }}
116
+
117
+ .controls label {{
118
+ color: #f0f0f0;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ cursor: pointer;
123
+ }}
124
+
125
+ .controls input[type="checkbox"] {{
126
+ cursor: pointer;
127
+ }}
128
+
129
+ .hidden {{
130
+ display: none !important;
131
+ }}
132
+
133
+ .entry {{
134
+ margin-bottom: 15px;
135
+ padding-left: 10px;
136
+ }}
137
+
138
+ .entry.faded {{
139
+ opacity: 0.4;
140
+ }}
141
+
142
+ .entry.important {{
143
+ opacity: 1.0;
144
+ }}
145
+
146
+ .entry.user .entity-header {{
147
+ color: #00bfff;
148
+ }}
149
+
150
+ .entry.assistant .entity-header {{
151
+ color: #ff6b6b;
152
+ }}
153
+
154
+ .entry.llm .entity-header {{
155
+ color: #00ff00;
156
+ }}
157
+
158
+ .entry.agent .entity-header {{
159
+ color: #ff9500;
160
+ }}
161
+
162
+ .entry.system .entity-header {{
163
+ color: #888;
164
+ }}
165
+
166
+ .entry.other .entity-header {{
167
+ color: #999;
168
+ }}
169
+
170
+ .entity-header {{
171
+ font-weight: bold;
172
+ margin-bottom: 5px;
173
+ cursor: pointer;
174
+ }}
175
+
176
+ .entity-header:hover {{
177
+ opacity: 0.8;
178
+ }}
179
+
180
+ .header-main {{
181
+ /* Removed text-transform to preserve tool name casing */
182
+ display: inline;
183
+ }}
184
+
185
+ .header-content {{
186
+ margin-left: 30px;
187
+ opacity: 0.7;
188
+ font-weight: normal;
189
+ font-style: italic;
190
+ display: block;
191
+ }}
192
+
193
+ .entry-content {{
194
+ margin-left: 20px;
195
+ margin-top: 5px;
196
+ }}
197
+
198
+ .entry-content.collapsed {{
199
+ display: none;
200
+ }}
201
+
202
+ .collapsible {{
203
+ margin: 5px 0;
204
+ margin-left: 20px;
205
+ }}
206
+
207
+ .toggle {{
208
+ cursor: pointer;
209
+ user-select: none;
210
+ color: #00ff00;
211
+ display: inline-block;
212
+ width: 25px;
213
+ font-family: monospace;
214
+ margin-right: 5px;
215
+ }}
216
+
217
+ .toggle:hover {{
218
+ color: #00ff00;
219
+ text-shadow: 0 0 5px #00ff00;
220
+ }}
221
+
222
+ .content {{
223
+ margin-left: 25px;
224
+ margin-top: 5px;
225
+ white-space: pre-wrap;
226
+ word-wrap: break-word;
227
+ }}
228
+
229
+ .main-content {{
230
+ margin-top: 10px;
231
+ white-space: pre-wrap;
232
+ word-wrap: break-word;
233
+ }}
234
+
235
+ .collapsed .content {{
236
+ display: none;
237
+ }}
238
+
239
+ .tool-section {{
240
+ margin: 10px 0;
241
+ margin-left: 20px;
242
+ }}
243
+
244
+ .tool-name {{
245
+ color: #d4a017;
246
+ font-weight: bold;
247
+ }}
248
+
249
+ .tool-result {{
250
+ margin-left: 25px;
251
+ }}
252
+
253
+ .tool-result.success {{
254
+ color: #00ff00;
255
+ }}
256
+
257
+ .tool-result.error {{
258
+ color: #ff0000;
259
+ }}
260
+
261
+ .code-block {{
262
+ background-color: #2b2b2b;
263
+ border: 1px solid #444;
264
+ padding: 10px;
265
+ margin: 5px 0;
266
+ border-radius: 3px;
267
+ overflow-x: auto;
268
+ }}
269
+
270
+ .metadata {{
271
+ color: #888;
272
+ font-size: 0.9em;
273
+ margin-left: 25px;
274
+ }}
275
+
276
+
277
+ pre {{
278
+ margin: 0;
279
+ white-space: pre-wrap;
280
+ word-wrap: break-word;
281
+ }}
282
+ </style>
283
+ <script>
284
+ function toggleEntry(entryId) {{
285
+ const contentElement = document.getElementById(entryId + '_content');
286
+ const toggleElement = document.querySelector(
287
+ '#' + entryId + ' .entity-header .toggle'
288
+ );
289
+
290
+ if (!contentElement || !toggleElement) return;
291
+
292
+ if (contentElement.classList.contains('collapsed')) {{
293
+ contentElement.classList.remove('collapsed');
294
+ toggleElement.textContent = '[-]';
295
+ // Save expanded state
296
+ localStorage.setItem('expanded_' + entryId, 'true');
297
+ }} else {{
298
+ contentElement.classList.add('collapsed');
299
+ toggleElement.textContent = '[+]';
300
+ // Save collapsed state
301
+ localStorage.setItem('expanded_' + entryId, 'false');
302
+ }}
303
+ }}
304
+
305
+ function toggle(id) {{
306
+ const element = document.getElementById(id);
307
+ if (!element) return;
308
+
309
+ element.classList.toggle('collapsed');
310
+ const toggle = element.querySelector('.toggle');
311
+ if (toggle) {{
312
+ toggle.textContent = element.classList.contains('collapsed')
313
+ ? '[+]' : '[-]';
314
+ }}
315
+
316
+ // Save collapsed state for collapsible sections
317
+ localStorage.setItem(
318
+ 'collapsed_' + id, element.classList.contains('collapsed')
319
+ );
320
+ }}
321
+
322
+ let allExpanded = false;
323
+
324
+ function toggleAll() {{
325
+ const btn = document.getElementById('toggleAllBtn');
326
+ if (allExpanded) {{
327
+ collapseAll();
328
+ btn.textContent = 'Expand All';
329
+ allExpanded = false;
330
+ }} else {{
331
+ expandAll();
332
+ btn.textContent = 'Collapse All';
333
+ allExpanded = true;
334
+ }}
335
+ }}
336
+
337
+ function expandAll() {{
338
+ // Expand all visible main entries
339
+ const entries = document.querySelectorAll(
340
+ '.entry:not(.hidden) .entry-content'
341
+ );
342
+ entries.forEach(element => {{
343
+ element.classList.remove('collapsed');
344
+ }});
345
+
346
+ // Update all visible main entry toggles
347
+ const entryToggles = document.querySelectorAll(
348
+ '.entry:not(.hidden) .entity-header .toggle'
349
+ );
350
+ entryToggles.forEach(toggle => {{
351
+ toggle.textContent = '[-]';
352
+ }});
353
+
354
+ // Expand all visible sub-sections
355
+ const collapsibles = document.querySelectorAll(
356
+ '.entry:not(.hidden) .collapsible'
357
+ );
358
+ collapsibles.forEach(element => {{
359
+ element.classList.remove('collapsed');
360
+ const toggle = element.querySelector('.toggle');
361
+ if (toggle) {{
362
+ toggle.textContent = '[-]';
363
+ }}
364
+ }});
365
+ }}
366
+
367
+ function collapseAll() {{
368
+ // Collapse all visible entries
369
+ const entries = document.querySelectorAll(
370
+ '.entry:not(.hidden) .entry-content'
371
+ );
372
+ entries.forEach(element => {{
373
+ element.classList.add('collapsed');
374
+ }});
375
+
376
+ // Update all visible entry toggles
377
+ const entryToggles = document.querySelectorAll(
378
+ '.entry:not(.hidden) .entity-header .toggle'
379
+ );
380
+ entryToggles.forEach(toggle => {{
381
+ toggle.textContent = '[+]';
382
+ }});
383
+
384
+ // Collapse all visible sub-sections
385
+ const collapsibles = document.querySelectorAll(
386
+ '.entry:not(.hidden) .collapsible'
387
+ );
388
+ collapsibles.forEach(element => {{
389
+ element.classList.add('collapsed');
390
+ const toggle = element.querySelector('.toggle');
391
+ if (toggle) {{
392
+ toggle.textContent = '[+]';
393
+ }}
394
+ }});
395
+ }}
396
+
397
+ function filterEntries() {{
398
+ const checkbox = document.getElementById('filterCheckbox');
399
+ const entries = document.querySelectorAll('.entry');
400
+
401
+ // Save checkbox state to localStorage
402
+ localStorage.setItem('filterImportant', checkbox.checked);
403
+
404
+ if (checkbox.checked) {{
405
+ // Show only important entries
406
+ entries.forEach(entry => {{
407
+ const isImportant = entry.classList.contains('important');
408
+ if (isImportant) {{
409
+ entry.classList.remove('hidden');
410
+ }} else {{
411
+ entry.classList.add('hidden');
412
+ }}
413
+ }});
414
+ }} else {{
415
+ // Show all entries
416
+ entries.forEach(entry => {{
417
+ entry.classList.remove('hidden');
418
+ }});
419
+ }}
420
+
421
+ // Reset toggle button state
422
+ allExpanded = false;
423
+ document.getElementById('toggleAllBtn').textContent = 'Expand All';
424
+ }}
425
+
426
+ // Initialize all as collapsed on load
427
+ document.addEventListener('DOMContentLoaded', function() {{
428
+ collapseAll();
429
+
430
+ // Restore checkbox state from localStorage
431
+ const checkbox = document.getElementById('filterCheckbox');
432
+ const savedState = localStorage.getItem('filterImportant');
433
+ if (savedState !== null) {{
434
+ // Use saved state if it exists
435
+ checkbox.checked = savedState === 'true';
436
+ }}
437
+ // Apply filter based on checkbox state (default is checked)
438
+ if (checkbox.checked) {{
439
+ filterEntries();
440
+ }}
441
+
442
+ // Restore expanded states from localStorage
443
+ const entries = document.querySelectorAll('.entry');
444
+ entries.forEach(entry => {{
445
+ const entryId = entry.id;
446
+ const expandedState = localStorage.getItem('expanded_' + entryId);
447
+ if (expandedState === 'true') {{
448
+ const contentElement = document.getElementById(
449
+ entryId + '_content'
450
+ );
451
+ const toggleElement = entry.querySelector('.entity-header .toggle');
452
+ if (contentElement && toggleElement) {{
453
+ contentElement.classList.remove('collapsed');
454
+ toggleElement.textContent = '[-]';
455
+ }}
456
+ }}
457
+ }});
458
+
459
+ // Restore collapsible section states
460
+ const collapsibles = document.querySelectorAll('.collapsible');
461
+ collapsibles.forEach(collapsible => {{
462
+ const id = collapsible.id;
463
+ const collapsedState = localStorage.getItem('collapsed_' + id);
464
+ if (collapsedState === 'false') {{
465
+ collapsible.classList.remove('collapsed');
466
+ const toggle = collapsible.querySelector('.toggle');
467
+ if (toggle) {{
468
+ toggle.textContent = '[-]';
469
+ }}
470
+ }}
471
+ }});
472
+ }});
473
+ </script>
474
+ </head>
475
+ <body>
476
+ <div class="header">
477
+ <div class="header-line">
478
+ <div>{self.filename}</div>
479
+ <div id="timestamp">{timestamp}</div>
480
+ </div>
481
+ </div>
482
+
483
+ <div class="separator"></div>
484
+
485
+ <div class="controls">
486
+ <button id="toggleAllBtn" onclick="toggleAll()">Expand All</button>
487
+ <label style="margin-left: 20px;">
488
+ <input type="checkbox" id="filterCheckbox"
489
+ onchange="filterEntries()" checked>
490
+ Show only important responses
491
+ </label>
492
+ </div>
493
+
494
+ <div id="content">
495
+ """
496
+ try:
497
+ with open(self.file_path, "w", encoding="utf-8") as f:
498
+ f.write(html_content)
499
+ except Exception as e:
500
+ self.logger.error(f"Failed to write HTML header: {e}")
501
+
502
+ def log(self, fields: BaseModel) -> None:
503
+ """Log a message entry.
504
+
505
+ Args:
506
+ fields: ChatDocLoggerFields containing all log information
507
+ """
508
+ try:
509
+ entry_html = self._format_entry(fields)
510
+ self._append_to_file(entry_html)
511
+ self.entry_counter += 1
512
+ except Exception as e:
513
+ self.logger.error(f"Failed to log entry: {e}")
514
+
515
+ def _format_entry(self, fields: BaseModel) -> str:
516
+ """Format a log entry as HTML.
517
+
518
+ Args:
519
+ fields: ChatDocLoggerFields containing all log information
520
+
521
+ Returns:
522
+ HTML string for the entry
523
+ """
524
+ entry_id = f"entry_{self.entry_counter}"
525
+
526
+ # Get all relevant fields
527
+ responder = str(getattr(fields, "responder", "UNKNOWN"))
528
+ task_name = getattr(fields, "task_name", "root")
529
+ # TODO (CLAUDE) display sender_entity in parens right after responder,
530
+ # other than LLM, e.g. AGENT (USER)
531
+ sender_entity = str(getattr(fields, "sender_entity", ""))
532
+ tool = getattr(fields, "tool", "")
533
+ tool_type = getattr(fields, "tool_type", "")
534
+ content = getattr(fields, "content", "")
535
+ recipient = getattr(fields, "recipient", "")
536
+
537
+ # Determine CSS class based on responder
538
+ responder_upper = responder.upper()
539
+ if "USER" in responder_upper:
540
+ css_class = "user"
541
+ elif "LLM" in responder_upper:
542
+ css_class = "llm"
543
+ elif "AGENT" in responder_upper:
544
+ css_class = "agent"
545
+ elif "SYSTEM" in responder_upper:
546
+ css_class = "system"
547
+ else:
548
+ css_class = "other"
549
+
550
+ # Determine opacity class based on mark
551
+ mark = getattr(fields, "mark", "")
552
+ opacity_class = "important" if mark == "*" else "faded"
553
+
554
+ # Start building the entry
555
+ html_parts = [
556
+ f'<div class="entry {css_class} {opacity_class}" id="{entry_id}">'
557
+ ]
558
+
559
+ # Build smart header
560
+ entity_parts = [] # Main header line with entity info
561
+ content_preview = "" # Second line with content preview
562
+
563
+ # Add task name if not root
564
+ if task_name and task_name != "root":
565
+ entity_parts.append(task_name)
566
+
567
+ # Handle different responder types
568
+ if "USER" in responder_upper:
569
+ # Add responder with sender_entity in parens if different
570
+ if sender_entity and sender_entity != responder:
571
+ entity_parts.append(f"USER ({sender_entity})")
572
+ else:
573
+ entity_parts.append("USER")
574
+ # Show user input preview on second line
575
+ if content:
576
+ preview = content.replace("\n", " ")[:60]
577
+ if len(content) > 60:
578
+ preview += "..."
579
+ content_preview = f'"{preview}"'
580
+
581
+ elif "LLM" in responder_upper:
582
+ # Get model info from instance - don't uppercase it
583
+ model_label = "LLM"
584
+ if self.model_info:
585
+ model_label = f"LLM ({self.model_info})"
586
+
587
+ if tool and tool_type:
588
+ # LLM making a tool call - don't uppercase tool names
589
+ entity_parts.append(f"{model_label} → {tool_type}[{tool}]")
590
+ else:
591
+ # LLM generating plain text response
592
+ entity_parts.append(model_label)
593
+ if content:
594
+ # Show first line or first 60 chars on second line
595
+ first_line = content.split("\n")[0].strip()
596
+ if first_line:
597
+ preview = first_line[:60]
598
+ if len(first_line) > 60:
599
+ preview += "..."
600
+ content_preview = f'"{preview}"'
601
+
602
+ elif "AGENT" in responder_upper:
603
+ # Add responder with sender_entity in parens if different
604
+ agent_label = "AGENT"
605
+ if sender_entity and sender_entity != responder:
606
+ agent_label = f"AGENT ({sender_entity})"
607
+
608
+ # Agent responding (usually tool handling)
609
+ if tool:
610
+ entity_parts.append(f"{agent_label}[{tool}]")
611
+ # Show tool result preview on second line if available
612
+ if content:
613
+ preview = content.replace("\n", " ")[:40]
614
+ if len(content) > 40:
615
+ preview += "..."
616
+ content_preview = f"→ {preview}"
617
+ else:
618
+ entity_parts.append(agent_label)
619
+ if content:
620
+ preview = content[:50]
621
+ if len(content) > 50:
622
+ preview += "..."
623
+ content_preview = f'"{preview}"'
624
+
625
+ elif "SYSTEM" in responder_upper:
626
+ entity_parts.append("SYSTEM")
627
+ if content:
628
+ preview = content[:50]
629
+ if len(content) > 50:
630
+ preview += "..."
631
+ content_preview = f'"{preview}"'
632
+ else:
633
+ # Other responder types (like Task)
634
+ entity_parts.append(responder)
635
+
636
+ # Add recipient info if present
637
+ if recipient:
638
+ entity_parts.append(f"→ {recipient}")
639
+
640
+ # Construct the two-line header
641
+ header_main = " ".join(entity_parts)
642
+
643
+ # Build the header HTML with toggle, mark, and main content on same line
644
+ header_html = '<span class="toggle">[+]</span> '
645
+
646
+ # Note: opacity_class already determined above
647
+
648
+ # Add the main header content
649
+ header_html += f'<span class="header-main">{html.escape(header_main)}</span>'
650
+
651
+ # Add preview on second line if present
652
+ if content_preview:
653
+ header_html += (
654
+ f'\n <div class="header-content">'
655
+ f"{html.escape(content_preview)}</div>"
656
+ )
657
+
658
+ # Add expandable header
659
+ html_parts.append(
660
+ f"""
661
+ <div class="entity-header" onclick="toggleEntry('{entry_id}')">
662
+ {header_html}
663
+ </div>
664
+ <div id="{entry_id}_content" class="entry-content collapsed">"""
665
+ )
666
+
667
+ # Add collapsible sections
668
+
669
+ # System messages (if any)
670
+ system_content = self._extract_system_content(fields)
671
+ if system_content:
672
+ for idx, (label, content) in enumerate(system_content):
673
+ section_id = f"{entry_id}_system_{idx}"
674
+ html_parts.append(
675
+ self._create_collapsible_section(section_id, label, content)
676
+ )
677
+
678
+ # Tool information
679
+ tool = getattr(fields, "tool", None)
680
+ # Only add tool section if tool exists and is not empty
681
+ if tool and tool.strip():
682
+ tool_html = self._format_tool_section(fields, entry_id)
683
+ html_parts.append(tool_html)
684
+
685
+ # Main content
686
+ content = getattr(fields, "content", "")
687
+ if content and not (
688
+ tool and tool.strip()
689
+ ): # Don't duplicate content if it's a tool
690
+ html_parts.append(f'<div class="main-content">{html.escape(content)}</div>')
691
+
692
+ # Metadata (recipient, blocked)
693
+ metadata_parts = []
694
+ recipient = getattr(fields, "recipient", None)
695
+ if recipient:
696
+ metadata_parts.append(f"Recipient: {recipient}")
697
+
698
+ block = getattr(fields, "block", None)
699
+ if block:
700
+ metadata_parts.append(f"Blocked: {block}")
701
+
702
+ if metadata_parts:
703
+ html_parts.append(
704
+ f'<div class="metadata">{" | ".join(metadata_parts)}</div>'
705
+ )
706
+
707
+ # Close entry content div
708
+ html_parts.append("</div>") # Close entry-content
709
+ html_parts.append("</div>") # Close entry
710
+ return "\n".join(html_parts)
711
+
712
+ def _extract_system_content(self, fields: BaseModel) -> List[tuple[str, str]]:
713
+ """Extract system-related content from fields.
714
+
715
+ Returns:
716
+ List of (label, content) tuples
717
+ """
718
+ system_content = []
719
+
720
+ # Check for common system message patterns in content
721
+ content = getattr(fields, "content", "")
722
+ if content:
723
+ # Look for patterns like "[System Prompt]" or "System Reminder:"
724
+ if "[System Prompt]" in content or "System Prompt" in content:
725
+ system_content.append(("System Prompt", content))
726
+ elif "[System Reminder]" in content or "System Reminder" in content:
727
+ system_content.append(("System Reminder", content))
728
+
729
+ return system_content
730
+
731
+ def _create_collapsible_section(
732
+ self, section_id: str, label: str, content: str
733
+ ) -> str:
734
+ """Create a collapsible section.
735
+
736
+ Args:
737
+ section_id: Unique ID for the section
738
+ label: Label to display
739
+ content: Content to show when expanded
740
+
741
+ Returns:
742
+ HTML string for the collapsible section
743
+ """
744
+ return f"""
745
+ <div class="collapsible collapsed" id="{section_id}">
746
+ <span class="toggle" onclick="toggle('{section_id}')">[+]</span> {label}
747
+ <div class="content">{html.escape(content)}</div>
748
+ </div>"""
749
+
750
+ def _format_tool_section(self, fields: BaseModel, entry_id: str) -> str:
751
+ """Format tool-related information.
752
+
753
+ Args:
754
+ fields: ChatDocLoggerFields containing tool information
755
+ entry_id: Parent entry ID
756
+
757
+ Returns:
758
+ HTML string for the tool section
759
+ """
760
+ tool = getattr(fields, "tool", "")
761
+ tool_type = getattr(fields, "tool_type", "")
762
+ content = getattr(fields, "content", "")
763
+
764
+ tool_id = f"{entry_id}_tool_{self.tool_counter}"
765
+ self.tool_counter += 1
766
+
767
+ # Try to parse content as JSON for better formatting
768
+ try:
769
+ if content.strip().startswith("{"):
770
+ content_dict = json.loads(content)
771
+ formatted_content = json.dumps(content_dict, indent=2)
772
+ content_html = (
773
+ f'<pre class="code-block">{html.escape(formatted_content)}</pre>'
774
+ )
775
+ else:
776
+ content_html = html.escape(content)
777
+ except Exception:
778
+ content_html = html.escape(content)
779
+
780
+ # Build tool section
781
+ tool_name = f"{tool_type}({tool})" if tool_type else tool
782
+
783
+ return f"""
784
+ <div class="tool-section">
785
+ <div class="collapsible collapsed" id="{tool_id}">
786
+ <span class="toggle" onclick="toggle('{tool_id}')">[+]</span>
787
+ <span class="tool-name">{html.escape(tool_name)}</span>
788
+ <div class="content">{content_html}</div>
789
+ </div>
790
+ </div>"""
791
+
792
+ def _append_to_file(self, content: str) -> None:
793
+ """Append content to the HTML file.
794
+
795
+ Args:
796
+ content: HTML content to append
797
+ """
798
+ try:
799
+ with open(self.file_path, "a", encoding="utf-8") as f:
800
+ f.write(content + "\n")
801
+ f.flush()
802
+ except Exception as e:
803
+ self.logger.error(f"Failed to append to file: {e}")
804
+
805
+ def close(self) -> None:
806
+ """Close the HTML file with footer."""
807
+ footer = """
808
+ </div>
809
+ <script>
810
+ // Update message count
811
+ const header = document.querySelector('.header-line div:last-child');
812
+ if (header) {
813
+ const messageCount = document.querySelectorAll('.entry').length;
814
+ header.textContent = header.textContent.replace(
815
+ /\\d+ messages/, messageCount + ' messages'
816
+ );
817
+ }
818
+ </script>
819
+ </body>
820
+ </html>"""
821
+ try:
822
+ with open(self.file_path, "a", encoding="utf-8") as f:
823
+ f.write(footer)
824
+ except Exception as e:
825
+ self.logger.error(f"Failed to write HTML footer: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langroid
3
- Version: 0.56.18
3
+ Version: 0.57.0
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  Author-email: Prasad Chalasani <pchalasani@gmail.com>
6
6
  License: MIT
@@ -5,11 +5,11 @@ langroid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
6
6
  langroid/agent/base.py,sha256=exiOhO0L1StZ8ziPBnunHYiKFvEnRyaLnMpBrc8tyMw,86263
7
7
  langroid/agent/batch.py,sha256=wpE9RqCNDVDhAXkCB7wEqfCIEAi6qKcrhaZ-Zr9T4C0,21375
8
- langroid/agent/chat_agent.py,sha256=pBnLGlAA6d2MK_1qa4GyhFZHnDf_RrUDli7__PKRRz4,88956
9
- langroid/agent/chat_document.py,sha256=0e6zYkqIorMIVbCsxOul9ziwAPPOWDsBsRV9E8ux-WI,18055
8
+ langroid/agent/chat_agent.py,sha256=H_6IgyIcyZ1SPUpagxVZcWVTQrQw0EJaCeTX3yiwREU,88954
9
+ langroid/agent/chat_document.py,sha256=t18sfN7e-cxQVINHjIBIPhr37EJJe-1u7mugq10J9i8,19755
10
10
  langroid/agent/done_sequence_parser.py,sha256=oUPzQCkkAo-5qos3ndSV47Lre7O_LoGWwTybjE9sCwc,4381
11
11
  langroid/agent/openai_assistant.py,sha256=JkAcs02bIrgPNVvUWVR06VCthc5-ulla2QMBzux_q6o,34340
12
- langroid/agent/task.py,sha256=jdIp4sko2VNJ13pbQwkPp0WyLCl5cxnioSdB6KnVtgM,102050
12
+ langroid/agent/task.py,sha256=DUv4Q_VqLtwMejzJPd9QREx04RCIkoqI5G1755cLy1E,105932
13
13
  langroid/agent/tool_message.py,sha256=BhjP-_TfQ2tgxuY4Yo_JHLOwwt0mJ4BwjPnREvEY4vk,14744
14
14
  langroid/agent/xml_tool_message.py,sha256=oeBKnJNoGaKdtz39XoWGMTNlVyXew2MWH5lgtYeh8wQ,15496
15
15
  langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -78,7 +78,7 @@ langroid/language_models/client_cache.py,sha256=YtGcpalYkS_ckMU12J7VmUOGmVv1wzuL
78
78
  langroid/language_models/config.py,sha256=9Q8wk5a7RQr8LGMT_0WkpjY8S4ywK06SalVRjXlfCiI,378
79
79
  langroid/language_models/mock_lm.py,sha256=tA9JpURznsMZ59iRhFYMmaYQzAc0D0BT-PiJIV58sAk,4079
80
80
  langroid/language_models/model_info.py,sha256=LzRfZsWmOm7WF6KGJfcN0aVdRqk0URNuDGMMz6cFt50,17121
81
- langroid/language_models/openai_gpt.py,sha256=t2nCGKK-WCW8DLZzOstcXgH18QIEKXavp9bbc5BYZUo,89323
81
+ langroid/language_models/openai_gpt.py,sha256=fYRZlfpo1CF8ePF1Cuy2CtStxDhpq6AeTxwuqK-z_T8,89329
82
82
  langroid/language_models/provider_params.py,sha256=fX25NAmYUIc1-nliMKpmTGZO6D6RpyTXtSDdZCZdb5w,5464
83
83
  langroid/language_models/utils.py,sha256=n55Oe2_V_4VNGhytvPWLYC-0tFS07RTjN83KWl-p_MI,6032
84
84
  langroid/language_models/prompt_formatter/__init__.py,sha256=2-5cdE24XoFDhifOLl8yiscohil1ogbP1ECkYdBlBsk,372
@@ -117,6 +117,7 @@ langroid/utils/configuration.py,sha256=ZkHHkEeWuS-o3_S4g0SE0wz-UK_of23NOWve1kpQi
117
117
  langroid/utils/constants.py,sha256=CK09kda9bNDEhnwClq7ZTWZOh38guJlfcZ5hKUS1Ijo,1075
118
118
  langroid/utils/git_utils.py,sha256=WnflJ3R3owhlD0LNdSJakcKhExcEehE1UW5jYVQl8JY,7955
119
119
  langroid/utils/globals.py,sha256=Az9dOFqR6n9CoTYSqa2kLikQWS0oCQ9DFQIQAnG-2q8,1355
120
+ langroid/utils/html_logger.py,sha256=9FhSvg3-5wJp-wmuqykT3Ur8GHQa-BodVPUya-uyIrQ,27029
120
121
  langroid/utils/logging.py,sha256=RgfmWRWe178rQSd0gFrrSizHzD0mG-SA5FR92kNZ9Gk,7280
121
122
  langroid/utils/object_registry.py,sha256=iPz9GHzvmCeVoidB3JdAMEKcxJEqTdUr0otQEexDZ5s,2100
122
123
  langroid/utils/pandas_utils.py,sha256=IaEtdy4IkIh6fjc7XXpczwjhgWodoGmJX50LxoYSEeI,7280
@@ -138,7 +139,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
138
139
  langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
139
140
  langroid/vector_store/qdrantdb.py,sha256=ZYrT9mxoUCx_67Qzb5xnkWuFG12rfe30yAg4NgG2ueA,19168
140
141
  langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
141
- langroid-0.56.18.dist-info/METADATA,sha256=0Rdt3Rs9yJ_3_SKT2T4tvfpDuzxllUDG_d5oHuKZGLg,65745
142
- langroid-0.56.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
- langroid-0.56.18.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
144
- langroid-0.56.18.dist-info/RECORD,,
142
+ langroid-0.57.0.dist-info/METADATA,sha256=y3GSqdrxEQMaV2iMTmioT9KkTGBIo4oHoPRQexgCXdw,65744
143
+ langroid-0.57.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
144
+ langroid-0.57.0.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
145
+ langroid-0.57.0.dist-info/RECORD,,