Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions concordia/command_line_interface/concordia_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@
concordia-log components sim.json --component tension_tracker
concordia-log entities sim.json
concordia-log dump sim.json | jq '...'
concordia-log bundle sim.json --output sim_viewer.html

Add --json for structured JSON output.
"""

import argparse
import json
import pathlib
import re
import sys

from concordia.utils import log_viewer
from concordia.utils import structured_logging


Expand Down Expand Up @@ -366,6 +369,27 @@ def cmd_dump(args):
print(text)


def cmd_bundle(args):
"""Build a portable HTML viewer with the structured log embedded."""
log = _load_log(args.log_file)
input_path = pathlib.Path(args.log_file)
output_path = (
pathlib.Path(args.output)
if args.output
else input_path.with_name(f'{input_path.stem}_viewer.html')
)
output_path.write_text(
log_viewer.build_self_contained_viewer(
log, log_name=input_path.name
),
encoding='utf-8',
)
if args.json:
_print_json({'output': str(output_path)})
else:
print(f'Wrote self-contained log viewer to {output_path}')


def main(argv=None):
parser = argparse.ArgumentParser(
prog='concordia-log',
Expand All @@ -389,6 +413,7 @@ def main(argv=None):
' concordia-log components sim.json --entity Alice'
' --component __act__ --key Key --step 3\n'
' concordia-log dump sim.json | jq ".[] | .data.__act__.Value"\n'
' concordia-log bundle sim.json --output sim_viewer.html\n'
' concordia-log actions sim.json Alice | grep "hello"\n'
),
)
Expand Down Expand Up @@ -502,6 +527,17 @@ def main(argv=None):
p.add_argument('--step', type=int, help='Filter to a specific step')
p.add_argument('--entity', help='Filter to a specific entity')

p = subparsers.add_parser(
'bundle',
help='Build a portable HTML viewer with the log embedded',
)
p.add_argument('log_file', help='Path to structured log JSON file')
p.add_argument(
'-o',
'--output',
help='Output HTML path (default: <log_file>_viewer.html)',
)

args = parser.parse_args(argv)

commands = {
Expand All @@ -515,6 +551,7 @@ def main(argv=None):
'memories': cmd_memories,
'components': cmd_components,
'dump': cmd_dump,
'bundle': cmd_bundle,
}
commands[args.command](args)

Expand Down
51 changes: 51 additions & 0 deletions concordia/command_line_interface/concordia_log_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io
import json
import os
import pathlib
import sys
import tempfile

Expand Down Expand Up @@ -480,6 +481,56 @@ def test_dump_filter_entity(self):
self.assertTrue(all(e['entity_name'] == 'Alice' for e in data))


class BundleTest(absltest.TestCase):

def setUp(self):
super().setUp()
self._log = _create_sample_log()
self._path = _write_log_to_file(self._log)
output_fd, self._output = tempfile.mkstemp(suffix='.html')
os.close(output_fd)

def tearDown(self):
super().tearDown()
os.unlink(self._path)
if os.path.exists(self._output):
os.unlink(self._output)

def test_bundle_writes_self_contained_viewer(self):
output = _capture_output(
concordia_log.main,
['bundle', self._path, '--output', self._output],
)

self.assertIn(self._output, output)
html = pathlib.Path(self._output).read_text()
self.assertIn('Concordia Structured Log Viewer', html)
self.assertIn('let LOG_DATA = {', html)
self.assertIn('Alice said hello to Bob', html)
self.assertIn('Preloaded structured log:', html)

def test_bundle_escapes_script_end_tags_in_log_content(self):
self._log.add_entry(
step=3,
timestamp='2024-01-01T10:02:00',
entity_name='Alice',
component_name='ActComponent',
entry_type='entity',
summary='</script><script>alert("unsafe")</script>',
raw_data={'value': 'safe'},
)
with open(self._path, 'w') as log_file:
log_file.write(self._log.to_json())

concordia_log.main(
['bundle', self._path, '--output', self._output]
)

html = pathlib.Path(self._output).read_text()
self.assertNotIn('</script><script>alert', html)
self.assertIn('<\\/script><script>alert', html)


class ImageStrippingTest(absltest.TestCase):

def setUp(self):
Expand Down
10 changes: 10 additions & 0 deletions concordia/docs/skills/analyze-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ For **human users**, the easiest way to browse logs interactively is to open
`utils/log_viewer.html` in a browser and load the structured log JSON file.
The viewer renders inline images and supports lazy expansion of large entries.

To create a portable HTML file with the structured log already loaded, use:

```bash
concordia-log bundle sim_structured.json --output sim_viewer.html
```

The generated viewer is self-contained and can be opened offline or shared as a
single file. It includes all component data, prompts, and memories from the
source log, so review the log contents before sharing it.

---

## Secondary Approach: Python API
Expand Down
30 changes: 30 additions & 0 deletions concordia/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Concordia utilities

## Portable structured log viewer

The repository includes an interactive HTML viewer for structured simulation
logs. To combine a structured JSON log and the viewer into one portable file,
run:

```shell
concordia-log bundle simulation_structured.json
```

This writes `simulation_structured_viewer.html` beside the input file. Open the
HTML file in any modern browser; it does not need a server or the original JSON
file.

Choose a different output path with `--output`:

```shell
concordia-log bundle simulation_structured.json \
--output reports/simulation.html
```

The resulting file contains the complete structured log, including component
data, prompts, memories, content references, and inline images. Be careful when
sharing it: private model context contained in the JSON is also contained in the
HTML.

To browse a log without creating a new file, open `log_viewer.html` and select
the structured JSON file using the file picker.
73 changes: 73 additions & 0 deletions concordia/utils/log_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2026 DeepMind Technologies Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Utilities for building portable structured log viewers."""

import importlib.resources
import json

from concordia.utils import structured_logging


_LOG_DATA_MARKER = 'let LOG_DATA = null;'
_SCRIPT_END_MARKER = '</script>'


def build_self_contained_viewer(
log: structured_logging.SimulationLog,
*,
log_name: str = 'structured log',
) -> str:
"""Embeds a structured log in the standard viewer HTML.

Args:
log: The structured simulation log to embed.
log_name: A display name for the preloaded log.

Returns:
A self-contained HTML document.

Raises:
ValueError: If the packaged viewer template is missing expected markers.
"""
viewer = (
importlib.resources.files('concordia.utils')
.joinpath('log_viewer.html')
.read_text(encoding='utf-8')
)
if _LOG_DATA_MARKER not in viewer:
raise ValueError('Log viewer template is missing the log data marker.')

# Escaping every start-tag slash prevents embedded log text from terminating
# the surrounding script element. ensure_ascii also makes JavaScript line
# separator characters safe inside the generated source.
embedded_log = json.dumps(log.to_dict(), ensure_ascii=True).replace(
'</', '<\\/'
)
viewer = viewer.replace(
_LOG_DATA_MARKER, f'let LOG_DATA = {embedded_log};', 1
)

script_index = viewer.rfind(_SCRIPT_END_MARKER)
if script_index < 0:
raise ValueError('Log viewer template is missing its closing script tag.')
display_name = json.dumps(log_name, ensure_ascii=True)
bootstrap = f"""
window.addEventListener('DOMContentLoaded', () => {{
document.getElementById('fileInfo').textContent =
'Preloaded structured log: ' + {display_name};
renderViewer();
}});
"""
return viewer[:script_index] + bootstrap + viewer[script_index:]
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _remove_excluded(description: str) -> str:
'Topic :: Scientific/Engineering :: Artificial Intelligence',
],
packages=setuptools.find_packages(include=['concordia', 'concordia.*']),
package_data={},
package_data={'concordia.utils': ['log_viewer.html']},
python_requires='>=3.12',
install_requires=(
'absl-py',
Expand Down
Loading