#!/usr/bin/env python3
"""
Interaction Logger for debugging MCP automation sessions
Captures detailed interaction flows with timestamps and structured data
"""
import json
import logging
import time
from datetime import datetime
from pathlib import Path
from typing import Any
from .models import LogEventData
class InteractionLogger:
"""Structured logger for debugging interactive automation sessions"""
def __init__(self, session_id: str, log_dir: str = "logs/interactions"):
self.session_id = session_id
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
# Create session-specific log file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_file = self.log_dir / f"session_{session_id}_{timestamp}.json"
# Initialize log structure
self.events: list[LogEventData] = []
self.session_start_time = time.time()
# Also create a human-readable log
self.readable_log = self.log_dir / f"session_{session_id}_{timestamp}.txt"
# Log session start
self.log_event("session_start", {"session_id": session_id})
def log_event(self, event_type: str, data: dict[str, Any] | None = None) -> None:
"""Log a structured event with timestamp"""
timestamp = time.time()
relative_time = timestamp - self.session_start_time
event = LogEventData(
event_type=event_type,
timestamp=timestamp,
relative_time=round(relative_time, 3),
data=data or {},
)
self.events.append(event)
# Write to JSON file immediately (for real-time debugging)
self._write_json()
# Write human-readable version
self._write_readable(event)
def log_screen_content(
self, content: str, description: str = "screen_capture"
) -> None:
"""Log current screen content"""
# Clean up ANSI escape codes for readable version
clean_content = self._clean_ansi(content)
self.log_event(
"screen_content",
{
"description": description,
"raw_content": content,
"clean_content": clean_content,
"content_length": len(content),
},
)
def log_input_sent(self, input_text: str, input_type: str = "user_input") -> None:
"""Log input sent to session"""
self.log_event(
"input_sent",
{
"input_text": input_text,
"input_type": input_type,
"input_length": len(input_text),
"input_repr": repr(input_text), # Shows escape sequences
},
)
def log_wait_start(self, pattern: str, timeout: int) -> None:
"""Log start of expect/wait operation"""
self.log_event(
"wait_start",
{"pattern": pattern, "timeout": timeout, "action": "waiting_for_pattern"},
)
def log_wait_result(
self,
success: bool,
matched_text: str | None = None,
timeout_occurred: bool = False,
) -> None:
"""Log result of expect/wait operation"""
self.log_event(
"wait_result",
{
"success": success,
"matched_text": matched_text,
"timeout_occurred": timeout_occurred,
},
)
def log_command_execution(
self, command: str, working_dir: str | None = None
) -> None:
"""Log command execution"""
self.log_event(
"command_execution", {"command": command, "working_directory": working_dir}
)
def log_session_state(
self, state: str, details: dict[str, Any] | None = None
) -> None:
"""Log session state changes"""
self.log_event("session_state", {"state": state, "details": details or {}})
def log_automation_step(
self, step_number: int, step_type: str, step_data: dict[str, Any]
) -> None:
"""Log automation step execution"""
self.log_event(
"automation_step",
{
"step_number": step_number,
"step_type": step_type,
"step_data": step_data,
},
)
def log_error(
self, error_type: str, error_message: str, stack_trace: str | None = None
) -> None:
"""Log errors and exceptions"""
self.log_event(
"error",
{
"error_type": error_type,
"error_message": error_message,
"stack_trace": stack_trace,
},
)
def close_session(
self, exit_code: int | None = None, final_output: str | None = None
) -> None:
"""Close the logging session"""
self.log_event(
"session_end",
{
"exit_code": exit_code,
"total_duration": time.time() - self.session_start_time,
"total_events": len(self.events),
},
)
if final_output:
self.log_screen_content(final_output, "final_output")
# Write final summary
self._write_summary()
def _write_json(self) -> None:
"""Write events to JSON file"""
try:
with open(self.log_file, "w") as f:
json.dump(
{
"session_id": self.session_id,
"session_start_time": self.session_start_time,
"events": [event.to_dict() for event in self.events],
},
f,
indent=2,
)
except Exception as e:
logging.error(f"Failed to write interaction log: {e}")
def _write_readable(self, event: LogEventData) -> None:
"""Write human-readable log entry"""
try:
with open(self.readable_log, "a") as f:
self._write_header(f, event)
self._write_event_data(f, event)
f.write("\n")
f.flush()
except Exception as e:
logging.error(f"Failed to write readable log: {e}")
def _write_header(self, f: Any, event: LogEventData) -> None:
"""Write log entry header"""
timestamp = datetime.fromtimestamp(event.timestamp).isoformat()
rel_time = event.relative_time
event_type = event.event_type
f.write(f"\n[{timestamp}] (+{rel_time}s) {event_type.upper()}\n")
f.write("-" * 60 + "\n")
def _write_event_data(self, f: Any, event: LogEventData) -> None:
"""Write event-specific data"""
data = event.data
event_type = event.event_type
if event_type == "screen_content":
self._write_screen_content(f, data)
elif event_type == "input_sent":
self._write_input_sent(f, data)
elif event_type == "wait_start":
self._write_wait_start(f, data)
elif event_type == "wait_result":
self._write_wait_result(f, data)
else:
self._write_generic_data(f, data)
def _write_screen_content(self, f: Any, data: dict[str, Any]) -> None:
"""Write screen content data"""
f.write(f"Description: {data.get('description', 'N/A')}\n")
f.write(f"Content Length: {data.get('content_length', 0)} chars\n")
f.write("Screen Content:\n")
f.write("=" * 40 + "\n")
f.write(data.get("clean_content", "") + "\n")
f.write("=" * 40 + "\n")
def _write_input_sent(self, f: Any, data: dict[str, Any]) -> None:
"""Write input sent data"""
f.write(f"Input Type: {data.get('input_type', 'unknown')}\n")
f.write(f"Input Text: {data.get('input_text', '')}\n")
f.write(f"Input Repr: {data.get('input_repr', '')}\n")
def _write_wait_start(self, f: Any, data: dict[str, Any]) -> None:
"""Write wait start data"""
f.write(f"Pattern: {data.get('pattern', '')}\n")
f.write(f"Timeout: {data.get('timeout', 0)}s\n")
def _write_wait_result(self, f: Any, data: dict[str, Any]) -> None:
"""Write wait result data"""
f.write(f"Success: {data.get('success', False)}\n")
f.write(f"Matched: {data.get('matched_text', 'N/A')}\n")
f.write(f"Timeout: {data.get('timeout_occurred', False)}\n")
def _write_generic_data(self, f: Any, data: dict[str, Any]) -> None:
"""Write generic data dump"""
for key, value in data.items():
f.write(f"{key}: {value}\n")
def _write_summary(self) -> None:
"""Write session summary"""
try:
summary_file = self.log_dir / f"session_{self.session_id}_summary.txt"
with open(summary_file, "w") as f:
f.write("INTERACTION LOG SUMMARY\n")
f.write(f"Session ID: {self.session_id}\n")
f.write(f"Total Events: {len(self.events)}\n")
f.write(f"Duration: {time.time() - self.session_start_time:.2f}s\n")
f.write("Log Files:\n")
f.write(f" - JSON: {self.log_file}\n")
f.write(f" - Readable: {self.readable_log}\n")
f.write("\nEvent Types:\n")
# Count event types
event_counts: dict[str, int] = {}
for event in self.events:
event_type = event.event_type
event_counts[event_type] = event_counts.get(event_type, 0) + 1
for event_type, count in sorted(event_counts.items()):
f.write(f" - {event_type}: {count}\n")
except Exception as e:
logging.error(f"Failed to write summary: {e}")
@staticmethod
def _clean_ansi(text: str) -> str:
"""Remove ANSI escape codes for readable output"""
import re
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
return ansi_escape.sub("", text)
def get_log_files(self) -> dict[str, str]:
"""Get paths to all log files for this session"""
return {
"json": str(self.log_file),
"readable": str(self.readable_log),
"summary": str(self.log_dir / f"session_{self.session_id}_summary.txt"),
}