Auftragsbuch: 'feature/skill-inactive-filter' angenommen (Ein-Klick-Gate)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# Skript zur Filterung inaktiver Skills
|
||||
|
||||
## Zweck
|
||||
|
||||
Das Skript `filter_inactive_skills.py` identifiziert Skills aus dem Hermes Skill-Index, die **nicht** genutzt wurden und **keine** Media-Skills sind.
|
||||
|
||||
## Kriterien
|
||||
|
||||
Ein Skill gilt als inaktiv, wenn **mindestens eines** der folgenden Kriterien zutrifft:
|
||||
- `use_count == 0`
|
||||
- `last_used_at == null` (nie genutzt)
|
||||
|
||||
Media-Skills (die im `~/.hermes/skills/media/`-Verzeichnis liegen) werden **ausgeschlossen**:
|
||||
- gif-search
|
||||
- heartmula
|
||||
- songsee
|
||||
- youtube-content
|
||||
|
||||
## Ausgabe
|
||||
|
||||
Die Ergebnisse werden als Liste sortiert nach `last_used_at` (älteste Nutzung zuerst) ausgegeben. Skills, die **nie** genutzt wurden (`last_used_at == null`), erscheinen am Ende.
|
||||
|
||||
Beispiel-Ausgabe:
|
||||
```
|
||||
Inaktive Skills (use_count=0 oder last_used_at=null, ohne Media-Skills): 45
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
airtable use_count= 0 last_used_at=nie
|
||||
apple-notes use_count= 0 last_used_at=nie
|
||||
...
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
```bash
|
||||
python3 filter_inactive_skills.py
|
||||
```
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
t_47fb5aaa/
|
||||
├── filter_inactive_skills.py # Hauptskript
|
||||
└── README.md # Diese Dokumentation
|
||||
```
|
||||
|
||||
## Datenquelle
|
||||
|
||||
- Skill-Metadaten: `~/.hermes/skills/.usage.json`
|
||||
- Media-Skills: `~/.hermes/skills/media/`
|
||||
|
||||
## Historie
|
||||
|
||||
- 2026-07-17: Erstellt im Rahmen von Kanban-Aufgabe t_47fb5aaa
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skript zur Filterung inaktiver Skills.
|
||||
|
||||
Filtert Skills aus ~/.hermes/skills/.usage.json, die:
|
||||
- use_count == 0 ODER last_used_at == null
|
||||
- NICHT im media/ Verzeichnis liegen (Media-Skills werden ausgeschlossen)
|
||||
|
||||
Ausgabe: Liste sortiert nach last_used_at (älteste zuerst), nur Skills mit use_count 0.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def load_usage_data(path: Path) -> dict:
|
||||
"""Lädt die .usage.json-Datei."""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_media_skills(skills_dir: Path) -> set:
|
||||
"""Ermittelt alle Media-Skills aus dem media/ Verzeichnis."""
|
||||
media_path = skills_dir / 'media'
|
||||
if not media_path.is_dir():
|
||||
return set()
|
||||
|
||||
media_skills = set()
|
||||
for item in media_path.iterdir():
|
||||
if item.is_dir():
|
||||
media_skills.add(item.name)
|
||||
return media_skills
|
||||
|
||||
|
||||
def is_media_skill(skill_name: str, media_skills: set) -> bool:
|
||||
"""Prüft, ob ein Skill ein Media-Skill ist."""
|
||||
return skill_name in media_skills
|
||||
|
||||
|
||||
def parse_datetime(dt_str: str | None) -> datetime | None:
|
||||
"""Parsed einen datetime-string im ISO-Format (mit timezone)."""
|
||||
if dt_str is None:
|
||||
return None
|
||||
# Entferne den timezone-Offset für datetime.fromisoformat (Python 3.11+ supportet +00:00)
|
||||
# Aber da wir nur sortieren, reicht es, den String direkt zu nutzen
|
||||
try:
|
||||
return datetime.fromisoformat(dt_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def filter_inactive_skills(usage: dict, media_skills: set) -> list:
|
||||
"""
|
||||
Filtert inaktive Skills.
|
||||
|
||||
Kriterien:
|
||||
- use_count == 0 ODER last_used_at == null
|
||||
- NICHT in media_skills
|
||||
|
||||
Gibt Liste zurück mit (skill_name, use_count, last_used_at)
|
||||
"""
|
||||
inactive = []
|
||||
|
||||
for skill_name, data in usage.items():
|
||||
if is_media_skill(skill_name, media_skills):
|
||||
continue
|
||||
|
||||
use_count = data.get('use_count', 0)
|
||||
last_used_at = data.get('last_used_at')
|
||||
|
||||
# Filter: use_count == 0 ODER last_used_at == null
|
||||
if use_count == 0 or last_used_at is None:
|
||||
inactive.append({
|
||||
'name': skill_name,
|
||||
'use_count': use_count,
|
||||
'last_used_at': last_used_at
|
||||
})
|
||||
|
||||
# Sortieren nach last_used_at (älteste zuerst, None am Ende)
|
||||
def sort_key(item):
|
||||
dt = parse_datetime(item['last_used_at'])
|
||||
if dt is None:
|
||||
return (1, '') # None am Ende
|
||||
return (0, dt.isoformat())
|
||||
|
||||
inactive.sort(key=sort_key)
|
||||
return inactive
|
||||
|
||||
|
||||
def print_results(skills: list) -> None:
|
||||
"""Gibt die Ergebnisse lesbar aus."""
|
||||
if not skills:
|
||||
print("Keine inaktiven Skills gefunden.")
|
||||
return
|
||||
|
||||
print(f"Inaktive Skills (use_count=0 oder last_used_at=null, ohne Media-Skills): {len(skills)}\n")
|
||||
print("-" * 80)
|
||||
|
||||
for skill in skills:
|
||||
name = skill['name']
|
||||
use_count = skill['use_count']
|
||||
last_used = skill['last_used_at'] if skill['last_used_at'] else 'nie'
|
||||
print(f" {name:30} use_count={use_count:3} last_used_at={last_used}")
|
||||
|
||||
print("-" * 80)
|
||||
|
||||
|
||||
def main():
|
||||
skills_dir = Path.home() / '.hermes' / 'skills'
|
||||
usage_path = skills_dir / '.usage.json'
|
||||
|
||||
if not usage_path.exists():
|
||||
print(f"Fehler: {usage_path} nicht gefunden.")
|
||||
return
|
||||
|
||||
usage = load_usage_data(usage_path)
|
||||
media_skills = get_media_skills(skills_dir)
|
||||
inactive_skills = filter_inactive_skills(usage, media_skills)
|
||||
print_results(inactive_skills)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user