Skip to main content
Glama
history.py12.6 kB
""" Session history management for Promptheus. Handles saving and retrieving prompt refinement history. """ import json import logging import os import sys from dataclasses import dataclass, asdict from datetime import datetime from pathlib import Path from typing import List, Optional, Tuple from promptheus.utils import sanitize_error_message logger = logging.getLogger(__name__) def get_default_history_dir() -> Path: """ Get the default history directory based on platform. Can be overridden with PROMPTHEUS_HISTORY_DIR environment variable. Returns: Path: Platform-appropriate history directory - Unix/Linux/Mac: ~/.promptheus - Windows: %APPDATA%/promptheus """ # Check for environment variable override override = os.getenv("PROMPTHEUS_HISTORY_DIR") if override: return Path(override).expanduser() if sys.platform == "win32": # On Windows, use AppData/Roaming appdata = os.environ.get("APPDATA") if appdata: return Path(appdata) / "promptheus" # Fallback to home directory if APPDATA not set return Path.home() / "promptheus" else: # Unix-like systems: use hidden directory in home return Path.home() / ".promptheus" @dataclass class HistoryEntry: """Represents a single history entry.""" timestamp: str original_prompt: str refined_prompt: str task_type: Optional[str] = None provider: Optional[str] = None model: Optional[str] = None def to_dict(self): """Convert to dictionary.""" return asdict(self) @classmethod def from_dict(cls, data: dict) -> "HistoryEntry": """Create from dictionary.""" return cls(**data) class PromptHistory: """Manages prompt history storage and retrieval.""" def __init__(self, history_dir: Optional[Path] = None, config=None): """ Initialize history manager. Args: history_dir: Directory to store history files. Defaults to platform-specific location: - Unix/Linux/Mac: ~/.promptheus - Windows: %APPDATA%/promptheus config: Configuration object to check if history is enabled """ if history_dir is None: history_dir = get_default_history_dir() self.history_dir = history_dir self.history_file = self.history_dir / "history.jsonl" # JSONL format self.prompt_history_file = self.history_dir / "prompt_history.txt" self.config = config # Create directory if it doesn't exist self._ensure_directory() @property def enabled(self) -> bool: """Check if history persistence is enabled.""" if self.config is None: # Fallback to enabled if no config provided (backward compatibility) return True return self.config.history_enabled def _ensure_directory(self) -> None: """Create history directory if it doesn't exist.""" try: self.history_dir.mkdir(parents=True, exist_ok=True) except PermissionError: logger.error( "Permission denied when creating history directory at %s", self.history_dir, ) except OSError as exc: logger.error( "Failed to create history directory (%s): %s", self.history_dir, sanitize_error_message(str(exc)), ) def save_entry( self, original_prompt: str, refined_prompt: str, task_type: Optional[str] = None, provider: Optional[str] = None, model: Optional[str] = None ) -> None: """ Save a history entry in O(1) time using append-only JSONL format. Args: original_prompt: The original user prompt refined_prompt: The final refined/accepted prompt task_type: Type of task (analysis, generation, etc.) provider: The provider used for the prompt model: The model used for the prompt """ # Check if history is enabled if not self.enabled: logger.debug("History persistence is disabled - skipping save") return entry = HistoryEntry( timestamp=datetime.now().isoformat(), original_prompt=original_prompt, refined_prompt=refined_prompt, task_type=task_type, provider=provider, model=model ) try: # Append entry to JSONL file (O(1) operation) # Use append mode which should be atomic on most systems with open(self.history_file, 'a', encoding='utf-8') as f: json.dump(entry.to_dict(), f) f.write('\n') f.flush() # Ensure data is written immediately # Also save to prompt history file for arrow key navigation self._append_to_prompt_history(original_prompt) logger.debug(f"Saved history entry: {entry.timestamp}") except Exception as exc: logger.error("Failed to save history entry: %s", sanitize_error_message(str(exc))) def _load_history(self) -> List[HistoryEntry]: """Load history from JSONL file.""" if not self.history_file.exists(): return [] try: history = [] with open(self.history_file, 'r', encoding='utf-8') as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line: continue try: entry_data = json.loads(line) history.append(HistoryEntry.from_dict(entry_data)) except json.JSONDecodeError as exc: logger.warning(f"Failed to parse history line {line_num}: {exc}") continue return history except OSError as exc: logger.error( "Failed to read history file (%s): %s", self.history_file, sanitize_error_message(str(exc)), ) return [] def _append_to_prompt_history(self, prompt: str) -> None: """Append a prompt to the prompt history file for arrow key navigation.""" # Check if history is enabled if not self.enabled: logger.debug("History persistence is disabled - skipping prompt history append") return try: with open(self.prompt_history_file, 'a', encoding='utf-8') as f: # Write prompt on a single line with proper escaping # Escape control characters that would break the one-line-per-entry format sanitized = prompt.replace('\\', '\\\\').replace('\r', '\\r').replace('\n', '\\n') f.write(f"{sanitized}\n") except OSError as exc: logger.error( "Failed to append to prompt history file (%s): %s", self.prompt_history_file, sanitize_error_message(str(exc)), ) def get_recent(self, limit: int = 20) -> List[HistoryEntry]: """ Get recent history entries. Args: limit: Maximum number of entries to return Returns: List of history entries, most recent first """ history = self._load_history() return list(reversed(history[-limit:])) def get_all(self) -> List[HistoryEntry]: """Get all history entries, most recent first.""" history = self._load_history() return list(reversed(history)) def get_total_count(self) -> int: """ Get the total number of history entries. Returns: Total number of history entries """ history = self._load_history() return len(history) def get_paginated(self, offset: int = 0, limit: int = 50) -> Tuple[List[HistoryEntry], int]: """ Get paginated history entries. Args: offset: Number of entries to skip limit: Maximum number of entries to return Returns: Tuple of (list of entries, total count) """ all_entries = self.get_all() total_count = len(all_entries) paginated_entries = all_entries[offset:offset+limit] return paginated_entries, total_count def get_all(self) -> List[HistoryEntry]: """Get all history entries, most recent first.""" history = self._load_history() return list(reversed(history)) def get_by_index(self, index: int) -> Optional[HistoryEntry]: """ Get a history entry by index (1-based, from most recent). Args: index: 1-based index (1 = most recent) Returns: History entry or None if index is out of range """ if not self.history_file.exists(): return None try: # For JSONL format, we can read line by line to find the target index # We need to read from the beginning and keep track of line count current_index = 0 lines = [] with open(self.history_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue current_index += 1 lines.append(line) # Keep only the most recent entries up to the target index if len(lines) > index: lines.pop(0) # Remove oldest line to maintain size if 1 <= index <= len(lines): target_line = lines[-index] # Get the line from end (most recent) entry_data = json.loads(target_line) return HistoryEntry.from_dict(entry_data) return None except (OSError, json.JSONDecodeError, IndexError) as exc: logger.warning(f"Failed to get history entry by index {index}: {exc}") return None def delete_by_timestamp(self, timestamp: str) -> bool: """ Delete a history entry by timestamp. Args: timestamp: ISO format timestamp of the entry to delete Returns: True if entry was deleted, False if not found """ if not self.history_file.exists(): return False try: # Load all entries entries = self._load_history() # Filter out the entry to delete filtered_entries = [e for e in entries if e.timestamp != timestamp] # Check if anything was removed if len(filtered_entries) == len(entries): return False # Entry not found # Rewrite the file without the deleted entry with open(self.history_file, 'w', encoding='utf-8') as f: for entry in filtered_entries: json.dump(entry.to_dict(), f) f.write('\n') logger.info(f"Deleted history entry: {timestamp}") return True except OSError as exc: logger.error( "Failed to delete history entry: %s", sanitize_error_message(str(exc)), ) return False def clear(self) -> None: """Clear all history.""" try: if self.history_file.exists(): self.history_file.unlink() if self.prompt_history_file.exists(): self.prompt_history_file.unlink() logger.info("History cleared") except OSError as exc: logger.error( "Failed to clear history files: %s", sanitize_error_message(str(exc)), ) def get_prompt_history_file(self) -> Path: """Get the path to the prompt history file for arrow key navigation.""" return self.prompt_history_file # Global instance _history_instance: Optional[PromptHistory] = None def get_history(config=None) -> PromptHistory: """ Get the global history instance. Args: config: Optional configuration object to check history settings Returns: PromptHistory instance with config applied if provided """ global _history_instance if _history_instance is None or config is not None: _history_instance = PromptHistory(config=config) return _history_instance

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abhichandra21/Promptheus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server