Skip to content

Commit 0a0d3fb

Browse files
jzleibocopybara-github
authored andcommitted
Improve the new structured logging library and remove functions used only for comparison to the old format.
PiperOrigin-RevId: 863381218 Change-Id: Ic596ac7cf91e106aa1cde22b4c8712394f8da1e1
1 parent 0abbaaf commit 0a0d3fb

File tree

2 files changed

+52
-274
lines changed

2 files changed

+52
-274
lines changed

concordia/utils/structured_logging.py

Lines changed: 50 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
import threading
3232
from typing import Any
3333

34-
from concordia.utils import html as html_lib
35-
3634
_DEFAULT_MIN_CHUNK_LENGTH = 50
3735

3836

@@ -554,62 +552,7 @@ def from_raw_log(cls, raw_log: list[Mapping[str, Any]]) -> SimulationLog:
554552

555553
return log
556554

557-
def to_raw_log(self) -> list[dict[str, Any]]:
558-
"""Export the log in the old raw_log format for backward compatibility.
559-
560-
This produces a list of dicts matching the format used by the old
561-
logging system, enabling gradual migration.
562-
563-
Returns:
564-
List of log entries in the existing format.
565-
"""
566-
raw_log: list[dict[str, Any]] = []
567-
568-
for step in self.get_steps():
569-
entries = self.get_entries_by_step(step)
570-
if not entries:
571-
continue
572-
573-
entry_dict: dict[str, Any] = {'Step': step}
574-
summaries = []
575-
576-
for entry in entries:
577-
summaries.append(entry.summary)
578-
579-
if entry.entry_type == 'entity':
580-
key = f'Entity [{entry.entity_name}]'
581-
else:
582-
key = entry.entity_name
583-
584-
reconstructed = self.reconstruct_value(entry.deduplicated_data)
585-
if reconstructed.get('value'):
586-
entry_dict[key] = reconstructed['value']
587-
588-
entry_dict['Summary'] = next((s for s in summaries if s), f'Step {step}')
589-
raw_log.append(entry_dict)
590-
591-
return raw_log
592-
593555
def to_html(self, title: str = 'Simulation Log') -> str:
594-
"""Render the log to HTML matching the existing format.
595-
596-
Uses entity memories and game master memories if they were attached
597-
when the log was created from Simulation.play().
598-
599-
Args:
600-
title: Title for the HTML page.
601-
602-
Returns:
603-
Complete HTML string with tabs for GM log, entities, and memories.
604-
"""
605-
return render_simulation_log_to_html(
606-
simulation_log=self,
607-
entity_memories=self._entity_memories or None,
608-
game_master_memories=self._game_master_memories or None,
609-
title=title,
610-
)
611-
612-
def to_dynamic_html(self, title: str = 'Simulation Log') -> str:
613556
"""Render the log to HTML with dynamic JavaScript-based deduplication.
614557
615558
This method generates HTML that stores content once in a JavaScript
@@ -631,68 +574,6 @@ def to_dynamic_html(self, title: str = 'Simulation Log') -> str:
631574
)
632575

633576

634-
def render_simulation_log_to_html(
635-
simulation_log: SimulationLog,
636-
entity_memories: dict[str, list[str]] | None = None,
637-
game_master_memories: list[str] | None = None,
638-
player_scores: dict[str, Any] | None = None,
639-
title: str = 'Simulation Log',
640-
) -> str:
641-
"""Render a SimulationLog to HTML matching the existing format.
642-
643-
This function produces HTML output that is visually identical to the
644-
existing logging system, ensuring a seamless transition.
645-
646-
Args:
647-
simulation_log: The structured log to render.
648-
entity_memories: Optional dict of entity_name -> list of memory strings.
649-
game_master_memories: Optional list of GM memory strings.
650-
player_scores: Optional dict of player scores to show in summary.
651-
title: Title for the HTML page.
652-
653-
Returns:
654-
Complete HTML string with tabs for GM log, entities, and memories.
655-
"""
656-
# Convert to raw_log format for HTML rendering
657-
raw_log = simulation_log.to_raw_log()
658-
659-
# Render main log
660-
results_log = html_lib.PythonObjectToHTMLConverter(raw_log).convert()
661-
662-
# Render entity memories
663-
player_logs = []
664-
player_log_names = []
665-
666-
if entity_memories:
667-
for entity_name, memories in entity_memories.items():
668-
player_html = html_lib.PythonObjectToHTMLConverter(memories).convert()
669-
player_logs.append(player_html)
670-
player_log_names.append(entity_name)
671-
672-
# Render game master memories
673-
if game_master_memories:
674-
gm_html = html_lib.PythonObjectToHTMLConverter(
675-
game_master_memories
676-
).convert()
677-
player_logs.append(gm_html)
678-
player_log_names.append('Game Master Memories')
679-
680-
# Build summary
681-
summary = ''
682-
if player_scores:
683-
summary = f'Player Scores: {player_scores}'
684-
685-
# Combine all pages
686-
tabbed_html = html_lib.combine_html_pages(
687-
[results_log, *player_logs],
688-
['Game Master log', *player_log_names],
689-
summary=summary,
690-
title=title,
691-
)
692-
693-
return html_lib.finalise_html(tabbed_html)
694-
695-
696577
def render_dynamic_html(
697578
simulation_log: SimulationLog,
698579
entity_memories: dict[str, list[str]] | None = None,
@@ -702,9 +583,9 @@ def render_dynamic_html(
702583
) -> str:
703584
"""Render the log to HTML with dynamic JavaScript-based content composition.
704585
705-
Unlike render_simulation_log_to_html, this function stores all unique content
706-
once in a JavaScript data block and uses JavaScript to dynamically compose
707-
the views. This achieves true deduplication in the HTML output.
586+
This function stores all unique content once in a JavaScript data block
587+
and uses JavaScript to dynamically compose the views. This achieves true
588+
deduplication in the HTML output.
708589
709590
Args:
710591
simulation_log: The log to render.
@@ -716,6 +597,7 @@ def render_dynamic_html(
716597
Returns:
717598
Complete HTML string with embedded data and dynamic rendering.
718599
"""
600+
719601
# Build the content store data for JavaScript
720602
content_store_data = simulation_log.content_store.to_dict()
721603

@@ -772,6 +654,12 @@ def render_dynamic_html(
772654
details { margin: 5px 0; }
773655
details summary { cursor: pointer; font-weight: bold; padding: 5px; background: #f0f0f0; border-radius: 4px; }
774656
details[open] summary { background: #e0e0e0; }
657+
/* Step-level details get special styling with a colored left border */
658+
details.step-details { margin: 10px 0; border-left: 3px solid #667eea; padding-left: 10px; }
659+
details.step-details > summary { background: #e8e8ff; font-size: 14px; }
660+
details.step-details[open] > summary { background: #d8d8ff; }
661+
/* Content inside steps is indented */
662+
details.step-details > details { margin-left: 15px; }
775663
.summary { padding: 10px; background: #e8f4f8; border-radius: 4px; margin-bottom: 15px; }
776664
h1 { color: #333; margin-bottom: 5px; }
777665
.subtitle { color: #666; margin-bottom: 20px; }
@@ -850,6 +738,7 @@ def render_dynamic_html(
850738
851739
// Recursively render any Python object as collapsible HTML
852740
// Mirrors PythonObjectToHTMLConverter logic
741+
// Handles _ref references by looking up content in CONTENT_STORE
853742
function renderObject(obj) {
854743
if (obj === null || obj === undefined) {
855744
return '';
@@ -872,6 +761,15 @@ def render_dynamic_html(
872761
}
873762
874763
if (typeof obj === 'object') {
764+
// Handle _ref references - lookup in CONTENT_STORE
765+
if (obj._ref && Object.keys(obj).length === 1) {
766+
const content = CONTENT_STORE[obj._ref];
767+
if (content !== undefined) {
768+
return escapeHtml(content);
769+
}
770+
return '[ref:' + obj._ref + ']';
771+
}
772+
875773
// Determine summary from special keys (like PythonObjectToHTMLConverter)
876774
let summary = '';
877775
if (obj.date) {
@@ -885,12 +783,12 @@ def render_dynamic_html(
885783
summary = escapeHtml(obj.Name);
886784
} else if (obj.Key) {
887785
summary = escapeHtml(obj.Key);
786+
} else if (obj.Value !== undefined || obj.value !== undefined) {
787+
// Entity data with a "Value" key - use "Details" as summary
788+
summary = 'Details';
888789
} else {
889-
// Use first key as summary
890-
const keys = Object.keys(obj);
891-
if (keys.length > 0) {
892-
summary = escapeHtml(keys[0]);
893-
}
790+
// For all other objects, use "Details" as a generic summary
791+
summary = 'Details';
894792
}
895793
896794
let html = '<details>';
@@ -946,50 +844,43 @@ def render_dynamic_html(
946844
947845
steps.forEach(step => {
948846
const entries = stepMap[step];
949-
html += '<details open>';
950-
html += '<summary><b>Step ' + step + '</b>';
951-
// Add summary from first entry if available
847+
848+
// Build step summary from entries
849+
let stepSummary = 'Step ' + step;
952850
if (entries.length > 0 && entries[0].summary) {
953-
html += ' --- ' + escapeHtml(entries[0].summary);
851+
stepSummary += ' --- ' + entries[0].summary;
954852
}
955-
html += '</summary>';
853+
854+
html += '<details class="step-details" open>';
855+
html += '<summary><b>' + escapeHtml(stepSummary) + '</b></summary>';
956856
957857
entries.forEach(entry => {
958-
html += '<div class="entry">';
959-
960-
// If there's raw_value in metadata, render it recursively
961-
if (entry.metadata && entry.metadata.raw_value) {
858+
// Create a label for this entry (like "Entity [name]" or component name)
859+
let entryLabel = entry.entity_name;
860+
if (entry.entry_type === 'entity') {
861+
entryLabel = 'Entity [' + entry.entity_name + ']';
862+
}
863+
864+
// If entry has deduplicated_data, render it as collapsible content
865+
if (entry.deduplicated_data && Object.keys(entry.deduplicated_data).length > 0) {
962866
html += '<details>';
963-
html += '<summary><b>' + escapeHtml(entry.metadata.raw_key || entry.entity_name) + '</b></summary>';
964-
html += renderObjectChildren(entry.metadata.raw_value);
867+
html += '<summary>' + escapeHtml(entryLabel) + '</summary>';
868+
// Render all the data in deduplicated_data recursively
869+
for (const [key, value] of Object.entries(entry.deduplicated_data)) {
870+
html += '<b><ul>' + escapeHtml(key) + '</b>';
871+
html += '<li>' + renderObject(value) + '</li></ul>';
872+
}
965873
html += '</details>';
966874
} else {
967-
// Simple entry without nested data
968-
html += '<span class="entry-entity">' + escapeHtml(entry.entity_name) + '</span>';
875+
// Simple entry with no data
876+
html += '<div class="entry">';
877+
html += '<span class="entry-entity">' + escapeHtml(entryLabel) + '</span>';
969878
html += ' <span class="entry-component">(' + escapeHtml(entry.component_name) + ')</span>';
970879
if (entry.summary) {
971880
html += '<div class="entry-summary">' + escapeHtml(entry.summary) + '</div>';
972881
}
973-
974-
// Show prompt/response if available
975-
const prompt = getContent(entry.prompt_id);
976-
const response = getContent(entry.response_id);
977-
978-
if (prompt || response) {
979-
html += '<details><summary>Details</summary>';
980-
if (prompt) {
981-
html += '<div class="content-label">Prompt</div>';
982-
html += '<div class="content-block">' + escapeHtml(prompt) + '</div>';
983-
}
984-
if (response) {
985-
html += '<div class="content-label">Response</div>';
986-
html += '<div class="content-block">' + escapeHtml(response) + '</div>';
987-
}
988-
html += '</details>';
989-
}
882+
html += '</div>';
990883
}
991-
992-
html += '</div>';
993884
});
994885
995886
html += '</details>';

0 commit comments

Comments
 (0)