Skip to content

Inspector

Trajectory Inspector

Full source code
#!/usr/bin/env python3
"""
Simple trajectory inspector for browsing agent conversation trajectories.

More information about the usage: [bold green] https://mini-swe-agent.com/latest/usage/inspector/ [/bold green].
"""

import json
import os
from pathlib import Path

import typer
from rich.text import Text
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Vertical, VerticalScroll
from textual.widgets import Footer, Header, Static

from minisweagent.agents.interactive_textual import _messages_to_steps

app = typer.Typer(rich_markup_mode="rich", add_completion=False)


class TrajectoryInspector(App):
    BINDINGS = [
        Binding("right,l", "next_step", "Step++"),
        Binding("left,h", "previous_step", "Step--"),
        Binding("0", "first_step", "Step=0"),
        Binding("$", "last_step", "Step=-1"),
        Binding("j,down", "scroll_down", "Scroll down"),
        Binding("k,up", "scroll_up", "Scroll up"),
        Binding("L", "next_trajectory", "Next trajectory"),
        Binding("H", "previous_trajectory", "Previous trajectory"),
        Binding("q", "quit", "Quit"),
    ]

    def __init__(self, trajectory_files: list[Path]):
        css_path = os.environ.get(
            "MSWEA_INSPECTOR_STYLE_PATH", str(Path(__file__).parent.parent / "config" / "mini.tcss")
        )
        self.__class__.CSS = Path(css_path).read_text()

        super().__init__()
        self.trajectory_files = trajectory_files
        self._i_trajectory = 0
        self._i_step = 0
        self.messages = []
        self.steps = []

        if trajectory_files:
            self._load_current_trajectory()

    # --- Basics ---

    @property
    def i_step(self) -> int:
        """Current step index."""
        return self._i_step

    @i_step.setter
    def i_step(self, value: int) -> None:
        """Set current step index, automatically clamping to valid bounds."""
        if value != self._i_step and self.n_steps > 0:
            self._i_step = max(0, min(value, self.n_steps - 1))
            self.query_one(VerticalScroll).scroll_to(y=0, animate=False)
            self.update_content()

    @property
    def n_steps(self) -> int:
        """Number of steps in current trajectory."""
        return len(self.steps)

    @property
    def i_trajectory(self) -> int:
        """Current trajectory index."""
        return self._i_trajectory

    @i_trajectory.setter
    def i_trajectory(self, value: int) -> None:
        """Set current trajectory index, automatically clamping to valid bounds."""
        if value != self._i_trajectory and self.n_trajectories > 0:
            self._i_trajectory = max(0, min(value, self.n_trajectories - 1))
            self._load_current_trajectory()
            self.query_one(VerticalScroll).scroll_to(y=0, animate=False)
            self.update_content()

    @property
    def n_trajectories(self) -> int:
        """Number of trajectory files."""
        return len(self.trajectory_files)

    def _load_current_trajectory(self) -> None:
        """Load the currently selected trajectory file."""
        if not self.trajectory_files:
            self.messages = []
            self.steps = []
            return

        trajectory_file = self.trajectory_files[self.i_trajectory]
        try:
            data = json.loads(trajectory_file.read_text())

            if isinstance(data, list):
                self.messages = data
            elif isinstance(data, dict) and "messages" in data:
                self.messages = data["messages"]
            else:
                raise ValueError("Unrecognized trajectory format")

            self.steps = _messages_to_steps(self.messages)
            self._i_step = 0
        except (json.JSONDecodeError, FileNotFoundError, ValueError) as e:
            self.messages = []
            self.steps = []
            self.notify(f"Error loading {trajectory_file.name}: {e}", severity="error")

    @property
    def current_trajectory_name(self) -> str:
        """Get the name of the current trajectory file."""
        if not self.trajectory_files:
            return "No trajectories"
        return self.trajectory_files[self.i_trajectory].name

    def compose(self) -> ComposeResult:
        yield Header()
        with Container(id="main"):
            with VerticalScroll():
                yield Vertical(id="content")
        yield Footer()

    def on_mount(self) -> None:
        self.update_content()

    def update_content(self) -> None:
        """Update the displayed content."""
        container = self.query_one("#content", Vertical)
        container.remove_children()

        if not self.steps:
            container.mount(Static("No trajectory loaded or empty trajectory"))
            self.title = "Trajectory Inspector - No Data"
            return

        for message in self.steps[self.i_step]:
            if isinstance(message["content"], list):
                content_str = "\n".join([item["text"] for item in message["content"]])
            else:
                content_str = str(message["content"])
            message_container = Vertical(classes="message-container")
            container.mount(message_container)
            role = message["role"].replace("assistant", "mini-swe-agent")
            message_container.mount(Static(role.upper(), classes="message-header"))
            message_container.mount(Static(Text(content_str, no_wrap=False), classes="message-content"))

        self.title = (
            f"Trajectory {self.i_trajectory + 1}/{self.n_trajectories} - "
            f"{self.current_trajectory_name} - "
            f"Step {self.i_step + 1}/{self.n_steps}"
        )

    # --- Navigation actions ---

    def action_next_step(self) -> None:
        self.i_step += 1

    def action_previous_step(self) -> None:
        self.i_step -= 1

    def action_first_step(self) -> None:
        self.i_step = 0

    def action_last_step(self) -> None:
        self.i_step = self.n_steps - 1

    def action_next_trajectory(self) -> None:
        self.i_trajectory += 1

    def action_previous_trajectory(self) -> None:
        self.i_trajectory -= 1

    def action_scroll_down(self) -> None:
        vs = self.query_one(VerticalScroll)
        vs.scroll_to(y=vs.scroll_target_y + 15)

    def action_scroll_up(self) -> None:
        vs = self.query_one(VerticalScroll)
        vs.scroll_to(y=vs.scroll_target_y - 15)


@app.command(help=__doc__)
def main(
    path: str = typer.Argument(".", help="Directory to search for trajectory files or specific trajectory file"),
) -> None:
    path_obj = Path(path)

    if path_obj.is_file():
        trajectory_files = [path_obj]
    elif path_obj.is_dir():
        trajectory_files = sorted(path_obj.rglob("*.traj.json"))
        if not trajectory_files:
            raise typer.BadParameter(f"No trajectory files found in '{path}'")
    else:
        raise typer.BadParameter(f"Error: Path '{path}' does not exist")

    inspector = TrajectoryInspector(trajectory_files)
    inspector.run()


if __name__ == "__main__":
    app()

minisweagent.run.inspector

Simple trajectory inspector for browsing agent conversation trajectories.

More information about the usage: [bold green] https://mini-swe-agent.com/latest/usage/inspector/ [/bold green].

app module-attribute

app = Typer(rich_markup_mode='rich', add_completion=False)

TrajectoryInspector

TrajectoryInspector(trajectory_files: list[Path])

Bases: App

Source code in src/minisweagent/run/inspector.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(self, trajectory_files: list[Path]):
    css_path = os.environ.get(
        "MSWEA_INSPECTOR_STYLE_PATH", str(Path(__file__).parent.parent / "config" / "mini.tcss")
    )
    self.__class__.CSS = Path(css_path).read_text()

    super().__init__()
    self.trajectory_files = trajectory_files
    self._i_trajectory = 0
    self._i_step = 0
    self.messages = []
    self.steps = []

    if trajectory_files:
        self._load_current_trajectory()

BINDINGS class-attribute instance-attribute

BINDINGS = [
    Binding("right,l", "next_step", "Step++"),
    Binding("left,h", "previous_step", "Step--"),
    Binding("0", "first_step", "Step=0"),
    Binding("$", "last_step", "Step=-1"),
    Binding("j,down", "scroll_down", "Scroll down"),
    Binding("k,up", "scroll_up", "Scroll up"),
    Binding("L", "next_trajectory", "Next trajectory"),
    Binding(
        "H", "previous_trajectory", "Previous trajectory"
    ),
    Binding("q", "quit", "Quit"),
]

trajectory_files instance-attribute

trajectory_files = trajectory_files

messages instance-attribute

messages = []

steps instance-attribute

steps = []

i_step property writable

i_step: int

Current step index.

n_steps property

n_steps: int

Number of steps in current trajectory.

i_trajectory property writable

i_trajectory: int

Current trajectory index.

n_trajectories property

n_trajectories: int

Number of trajectory files.

current_trajectory_name property

current_trajectory_name: str

Get the name of the current trajectory file.

compose

compose() -> ComposeResult
Source code in src/minisweagent/run/inspector.py
124
125
126
127
128
129
def compose(self) -> ComposeResult:
    yield Header()
    with Container(id="main"):
        with VerticalScroll():
            yield Vertical(id="content")
    yield Footer()

on_mount

on_mount() -> None
Source code in src/minisweagent/run/inspector.py
131
132
def on_mount(self) -> None:
    self.update_content()

update_content

update_content() -> None

Update the displayed content.

Source code in src/minisweagent/run/inspector.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def update_content(self) -> None:
    """Update the displayed content."""
    container = self.query_one("#content", Vertical)
    container.remove_children()

    if not self.steps:
        container.mount(Static("No trajectory loaded or empty trajectory"))
        self.title = "Trajectory Inspector - No Data"
        return

    for message in self.steps[self.i_step]:
        if isinstance(message["content"], list):
            content_str = "\n".join([item["text"] for item in message["content"]])
        else:
            content_str = str(message["content"])
        message_container = Vertical(classes="message-container")
        container.mount(message_container)
        role = message["role"].replace("assistant", "mini-swe-agent")
        message_container.mount(Static(role.upper(), classes="message-header"))
        message_container.mount(Static(Text(content_str, no_wrap=False), classes="message-content"))

    self.title = (
        f"Trajectory {self.i_trajectory + 1}/{self.n_trajectories} - "
        f"{self.current_trajectory_name} - "
        f"Step {self.i_step + 1}/{self.n_steps}"
    )

action_next_step

action_next_step() -> None
Source code in src/minisweagent/run/inspector.py
163
164
def action_next_step(self) -> None:
    self.i_step += 1

action_previous_step

action_previous_step() -> None
Source code in src/minisweagent/run/inspector.py
166
167
def action_previous_step(self) -> None:
    self.i_step -= 1

action_first_step

action_first_step() -> None
Source code in src/minisweagent/run/inspector.py
169
170
def action_first_step(self) -> None:
    self.i_step = 0

action_last_step

action_last_step() -> None
Source code in src/minisweagent/run/inspector.py
172
173
def action_last_step(self) -> None:
    self.i_step = self.n_steps - 1

action_next_trajectory

action_next_trajectory() -> None
Source code in src/minisweagent/run/inspector.py
175
176
def action_next_trajectory(self) -> None:
    self.i_trajectory += 1

action_previous_trajectory

action_previous_trajectory() -> None
Source code in src/minisweagent/run/inspector.py
178
179
def action_previous_trajectory(self) -> None:
    self.i_trajectory -= 1

action_scroll_down

action_scroll_down() -> None
Source code in src/minisweagent/run/inspector.py
181
182
183
def action_scroll_down(self) -> None:
    vs = self.query_one(VerticalScroll)
    vs.scroll_to(y=vs.scroll_target_y + 15)

action_scroll_up

action_scroll_up() -> None
Source code in src/minisweagent/run/inspector.py
185
186
187
def action_scroll_up(self) -> None:
    vs = self.query_one(VerticalScroll)
    vs.scroll_to(y=vs.scroll_target_y - 15)

main

main(
    path: str = Argument(
        ".",
        help="Directory to search for trajectory files or specific trajectory file",
    ),
) -> None
Source code in src/minisweagent/run/inspector.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@app.command(help=__doc__)
def main(
    path: str = typer.Argument(".", help="Directory to search for trajectory files or specific trajectory file"),
) -> None:
    path_obj = Path(path)

    if path_obj.is_file():
        trajectory_files = [path_obj]
    elif path_obj.is_dir():
        trajectory_files = sorted(path_obj.rglob("*.traj.json"))
        if not trajectory_files:
            raise typer.BadParameter(f"No trajectory files found in '{path}'")
    else:
        raise typer.BadParameter(f"Error: Path '{path}' does not exist")

    inspector = TrajectoryInspector(trajectory_files)
    inspector.run()