Add history learning module
This commit is contained in:
371
shellgenius/history.py
Normal file
371
shellgenius/history.py
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
"""Command history learning system for ShellGenius."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from shellgenius.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HistoryEntry:
|
||||||
|
"""A command history entry."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
timestamp: str
|
||||||
|
description: str
|
||||||
|
commands: List[str]
|
||||||
|
shell_type: str
|
||||||
|
usage_count: int = 1
|
||||||
|
tags: List[str] = field(default_factory=list)
|
||||||
|
success: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryStorage:
|
||||||
|
"""Storage management for command history."""
|
||||||
|
|
||||||
|
def __init__(self, storage_path: Optional[str] = None):
|
||||||
|
"""Initialize history storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_path: Path to history file
|
||||||
|
"""
|
||||||
|
config = get_config()
|
||||||
|
self.storage_path = storage_path or config.get(
|
||||||
|
"history.storage_path",
|
||||||
|
"~/.config/shellgenius/history.yaml",
|
||||||
|
)
|
||||||
|
self.storage_path = os.path.expanduser(self.storage_path)
|
||||||
|
self._ensure_storage_exists()
|
||||||
|
|
||||||
|
def _ensure_storage_exists(self) -> None:
|
||||||
|
"""Ensure storage directory exists."""
|
||||||
|
Path(self.storage_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not Path(self.storage_path).exists():
|
||||||
|
self._save({"entries": [], "metadata": {"version": "1.0"}})
|
||||||
|
|
||||||
|
def _load(self) -> Dict[str, Any]:
|
||||||
|
"""Load history from file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
History data dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.storage_path, "r") as f:
|
||||||
|
return yaml.safe_load(f) or {"entries": [], "metadata": {}}
|
||||||
|
except Exception:
|
||||||
|
return {"entries": [], "metadata": {}}
|
||||||
|
|
||||||
|
def _save(self, data: Dict[str, Any]) -> None:
|
||||||
|
"""Save history to file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: History data to save
|
||||||
|
"""
|
||||||
|
with open(self.storage_path, "w") as f:
|
||||||
|
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
|
def add_entry(self, entry: HistoryEntry) -> None:
|
||||||
|
"""Add a new history entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: HistoryEntry to add
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
data["entries"].append(
|
||||||
|
{
|
||||||
|
"id": entry.id,
|
||||||
|
"timestamp": entry.timestamp,
|
||||||
|
"description": entry.description,
|
||||||
|
"commands": entry.commands,
|
||||||
|
"shell_type": entry.shell_type,
|
||||||
|
"usage_count": entry.usage_count,
|
||||||
|
"tags": entry.tags,
|
||||||
|
"success": entry.success,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._save(data)
|
||||||
|
|
||||||
|
def get_entries(
|
||||||
|
self, limit: Optional[int] = None, offset: int = 0
|
||||||
|
) -> List[HistoryEntry]:
|
||||||
|
"""Get history entries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum entries to return
|
||||||
|
offset: Starting offset
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of HistoryEntry objects
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
entries = data.get("entries", [])
|
||||||
|
|
||||||
|
if offset > 0:
|
||||||
|
entries = entries[offset:]
|
||||||
|
if limit:
|
||||||
|
entries = entries[:limit]
|
||||||
|
|
||||||
|
return [
|
||||||
|
HistoryEntry(
|
||||||
|
id=e["id"],
|
||||||
|
timestamp=e["timestamp"],
|
||||||
|
description=e["description"],
|
||||||
|
commands=e["commands"],
|
||||||
|
shell_type=e["shell_type"],
|
||||||
|
usage_count=e.get("usage_count", 1),
|
||||||
|
tags=e.get("tags", []),
|
||||||
|
success=e.get("success", True),
|
||||||
|
)
|
||||||
|
for e in entries
|
||||||
|
]
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self, query: str, limit: int = 10
|
||||||
|
) -> List[HistoryEntry]:
|
||||||
|
"""Search history by query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query
|
||||||
|
limit: Maximum results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching HistoryEntry objects
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
results = []
|
||||||
|
query_lower = query.lower()
|
||||||
|
|
||||||
|
for entry in data.get("entries", []):
|
||||||
|
if (
|
||||||
|
query_lower in entry["description"].lower()
|
||||||
|
or any(query_lower in cmd.lower() for cmd in entry["commands"])
|
||||||
|
or any(query_lower in tag.lower() for tag in entry.get("tags", []))
|
||||||
|
):
|
||||||
|
results.append(
|
||||||
|
HistoryEntry(
|
||||||
|
id=entry["id"],
|
||||||
|
timestamp=entry["timestamp"],
|
||||||
|
description=entry["description"],
|
||||||
|
commands=entry["commands"],
|
||||||
|
shell_type=entry["shell_type"],
|
||||||
|
usage_count=e.get("usage_count", 1),
|
||||||
|
tags=e.get("tags", []),
|
||||||
|
success=e.get("success", True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_popular(self, limit: int = 10) -> List[HistoryEntry]:
|
||||||
|
"""Get most used entries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum entries to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of popular HistoryEntry objects
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
entries = [
|
||||||
|
HistoryEntry(
|
||||||
|
id=e["id"],
|
||||||
|
timestamp=e["timestamp"],
|
||||||
|
description=e["description"],
|
||||||
|
commands=e["commands"],
|
||||||
|
shell_type=e["shell_type"],
|
||||||
|
usage_count=e.get("usage_count", 1),
|
||||||
|
tags=e.get("tags", []),
|
||||||
|
success=e.get("success", True),
|
||||||
|
)
|
||||||
|
for e in data.get("entries", [])
|
||||||
|
]
|
||||||
|
entries.sort(key=lambda x: x.usage_count, reverse=True)
|
||||||
|
return entries[:limit]
|
||||||
|
|
||||||
|
def update_usage(self, entry_id: str) -> bool:
|
||||||
|
"""Increment usage count for an entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_id: Entry ID to update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if updated successfully
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
for entry in data.get("entries", []):
|
||||||
|
if entry["id"] == entry_id:
|
||||||
|
entry["usage_count"] = entry.get("usage_count", 0) + 1
|
||||||
|
self._save(data)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_entry(self, entry_id: str) -> bool:
|
||||||
|
"""Delete a history entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_id: Entry ID to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deleted successfully
|
||||||
|
"""
|
||||||
|
data = self._load()
|
||||||
|
original_len = len(data.get("entries", []))
|
||||||
|
data["entries"] = [e for e in data.get("entries", []) if e["id"] != entry_id]
|
||||||
|
if len(data["entries"]) < original_len:
|
||||||
|
self._save(data)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear all history."""
|
||||||
|
self._save({"entries": [], "metadata": {"version": "1.0"}})
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryLearner:
|
||||||
|
"""Learning from command history for personalized suggestions."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize history learner."""
|
||||||
|
self.storage = HistoryStorage()
|
||||||
|
self.config = get_config()
|
||||||
|
|
||||||
|
def learn(
|
||||||
|
self,
|
||||||
|
description: str,
|
||||||
|
commands: List[str],
|
||||||
|
shell_type: str = "bash",
|
||||||
|
tags: Optional[List[str]] = None,
|
||||||
|
) -> HistoryEntry:
|
||||||
|
"""Learn from a generated command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
description: Description of what was generated
|
||||||
|
commands: List of commands generated
|
||||||
|
shell_type: Shell type used
|
||||||
|
tags: Optional tags for categorization
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created HistoryEntry
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
entry = HistoryEntry(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
timestamp=datetime.utcnow().isoformat(),
|
||||||
|
description=description,
|
||||||
|
commands=commands,
|
||||||
|
shell_type=shell_type,
|
||||||
|
tags=tags or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.storage.add_entry(entry)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def find_similar(self, query: str, limit: int = 5) -> List[HistoryEntry]:
|
||||||
|
"""Find similar commands from history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Query to match against
|
||||||
|
limit: Maximum results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of similar HistoryEntry objects
|
||||||
|
"""
|
||||||
|
return self.storage.search(query, limit)
|
||||||
|
|
||||||
|
def suggest(
|
||||||
|
self, description: str, limit: int = 3
|
||||||
|
) -> List[HistoryEntry]:
|
||||||
|
"""Get personalized suggestions based on history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
description: Current task description
|
||||||
|
limit: Maximum suggestions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of suggested HistoryEntry objects
|
||||||
|
"""
|
||||||
|
similar = self.find_similar(description, limit)
|
||||||
|
popular = self.storage.get_popular(limit)
|
||||||
|
|
||||||
|
suggestions = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
for entry in similar + popular:
|
||||||
|
if entry.id not in seen:
|
||||||
|
suggestions.append(entry)
|
||||||
|
seen.add(entry.id)
|
||||||
|
if len(suggestions) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
def get_frequent_patterns(self) -> Dict[str, Any]:
|
||||||
|
"""Analyze frequent command patterns.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with pattern statistics
|
||||||
|
"""
|
||||||
|
entries = self.storage.get_entries(limit=100)
|
||||||
|
|
||||||
|
patterns = {
|
||||||
|
"shell_types": {},
|
||||||
|
"common_commands": {},
|
||||||
|
"frequent_tags": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
patterns["shell_types"][entry.shell_type] = (
|
||||||
|
patterns["shell_types"].get(entry.shell_type, 0) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for cmd in entry.commands:
|
||||||
|
first_word = cmd.strip().split()[0] if cmd.strip() else ""
|
||||||
|
if first_word:
|
||||||
|
patterns["common_commands"][first_word] = (
|
||||||
|
patterns["common_commands"].get(first_word, 0) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in entry.tags:
|
||||||
|
patterns["frequent_tags"][tag] = (
|
||||||
|
patterns["frequent_tags"].get(tag, 0) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return patterns
|
||||||
|
|
||||||
|
def update_from_feedback(
|
||||||
|
self, entry_id: str, was_useful: bool, tags: Optional[List[str]] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Update entry based on user feedback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_id: Entry ID
|
||||||
|
was_useful: Whether the suggestion was useful
|
||||||
|
tags: Optional tags to add
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if updated successfully
|
||||||
|
"""
|
||||||
|
if was_useful:
|
||||||
|
return self.storage.update_usage(entry_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_history_storage(storage_path: Optional[str] = None) -> HistoryStorage:
|
||||||
|
"""Get history storage instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_path: Optional storage path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HistoryStorage instance
|
||||||
|
"""
|
||||||
|
return HistoryStorage(storage_path)
|
||||||
Reference in New Issue
Block a user