From 7f72b274b54123674b270c2d5170e4281c006771 Mon Sep 17 00:00:00 2001 From: Tobi Date: Wed, 15 Jul 2026 07:48:35 +0200 Subject: [PATCH] =?UTF-8?q?skill-kanten-generator:=20Deploy-Skript=20f?= =?UTF-8?q?=C3=BCr=20automatische=20Skill-Verkn=C3=BCpfung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/journey-graph.json | 968 ++++++++++++++++++++++++++ deploy/skill-kanten-generator.py | 379 ++++++++++ deploy/test_skill_kanten_generator.py | 239 +++++++ 3 files changed, 1586 insertions(+) create mode 100644 deploy/journey-graph.json create mode 100644 deploy/skill-kanten-generator.py create mode 100644 deploy/test_skill_kanten_generator.py diff --git a/deploy/journey-graph.json b/deploy/journey-graph.json new file mode 100644 index 0000000..e954706 --- /dev/null +++ b/deploy/journey-graph.json @@ -0,0 +1,968 @@ +{ + "generated_at": "2026-07-13T02:08:40.843042+00:00", + "version": "1.0.0", + "nodes": [ + { + "id": "mission-control-model-management", + "label": "Mission Control Model Management", + "category": null, + "state": "active" + }, + { + "id": "user-preferences", + "label": "User Preferences", + "category": null, + "state": "active" + }, + { + "id": "airtable", + "label": "Airtable", + "category": null, + "state": "active" + }, + { + "id": "architecture-diagram", + "label": "Architecture Diagram", + "category": null, + "state": "active" + }, + { + "id": "arxiv", + "label": "Arxiv", + "category": null, + "state": "active" + }, + { + "id": "ascii-art", + "label": "Ascii Art", + "category": null, + "state": "active" + }, + { + "id": "ascii-video", + "label": "Ascii Video", + "category": null, + "state": "active" + }, + { + "id": "baoyu-infographic", + "label": "Baoyu Infographic", + "category": null, + "state": "active" + }, + { + "id": "blogwatcher", + "label": "Blogwatcher", + "category": null, + "state": "active" + }, + { + "id": "claude-code", + "label": "Claude Code", + "category": null, + "state": "active" + }, + { + "id": "claude-design", + "label": "Claude Design", + "category": null, + "state": "active" + }, + { + "id": "codebase-inspection", + "label": "Codebase Inspection", + "category": null, + "state": "active" + }, + { + "id": "codex", + "label": "Codex", + "category": null, + "state": "active" + }, + { + "id": "comfyui", + "label": "Comfyui", + "category": "creative", + "state": "active" + }, + { + "id": "computer-use", + "label": "Computer Use", + "category": null, + "state": "active" + }, + { + "id": "design-md", + "label": "Design Md", + "category": null, + "state": "active" + }, + { + "id": "dogfood", + "label": "Dogfood", + "category": null, + "state": "active" + }, + { + "id": "enhanced-chat-ui", + "label": "Enhanced Chat Ui", + "category": null, + "state": "active" + }, + { + "id": "evaluating-llms-harness", + "label": "Evaluating Llms Harness", + "category": null, + "state": "active" + }, + { + "id": "excalidraw", + "label": "Excalidraw", + "category": null, + "state": "active" + }, + { + "id": "findmy", + "label": "Findmy", + "category": null, + "state": "active" + }, + { + "id": "gif-search", + "label": "Gif Search", + "category": null, + "state": "active" + }, + { + "id": "github-auth", + "label": "Github Auth", + "category": null, + "state": "active" + }, + { + "id": "github-code-review", + "label": "Github Code Review", + "category": null, + "state": "active" + }, + { + "id": "github-issues", + "label": "Github Issues", + "category": null, + "state": "active" + }, + { + "id": "github-pr-workflow", + "label": "Github Pr Workflow", + "category": null, + "state": "active" + }, + { + "id": "github-repo-management", + "label": "Github Repo Management", + "category": null, + "state": "active" + }, + { + "id": "google-workspace", + "label": "Google Workspace", + "category": null, + "state": "active" + }, + { + "id": "heartmula", + "label": "Heartmula", + "category": null, + "state": "active" + }, + { + "id": "hermes-agent", + "label": "Hermes Agent", + "category": null, + "state": "active" + }, + { + "id": "hermes-agent-skill-authoring", + "label": "Hermes Agent Skill Authoring", + "category": null, + "state": "active" + }, + { + "id": "hermes-chat-context-issue", + "label": "Hermes Chat Context Issue", + "category": null, + "state": "active" + }, + { + "id": "himalaya", + "label": "Himalaya", + "category": null, + "state": "active" + }, + { + "id": "huggingface-hub", + "label": "Huggingface Hub", + "category": null, + "state": "active" + }, + { + "id": "humanizer", + "label": "Humanizer", + "category": "creative", + "state": "active" + }, + { + "id": "jupyter-live-kernel", + "label": "Jupyter Live Kernel", + "category": "data", + "state": "active" + }, + { + "id": "llama-cpp", + "label": "Llama Cpp", + "category": null, + "state": "active" + }, + { + "id": "llm-wiki", + "label": "Llm Wiki", + "category": "research", + "state": "active" + }, + { + "id": "local-dev-connection-setup", + "label": "Local Dev Connection Setup", + "category": null, + "state": "active" + }, + { + "id": "local-dev-sync", + "label": "Local Dev Sync", + "category": null, + "state": "active" + }, + { + "id": "manim-video", + "label": "Manim Video", + "category": null, + "state": "active" + }, + { + "id": "maps", + "label": "Maps", + "category": "productivity", + "state": "active" + }, + { + "id": "nano-pdf", + "label": "Nano Pdf", + "category": null, + "state": "active" + }, + { + "id": "news-summarization", + "label": "News Summarization", + "category": null, + "state": "active" + }, + { + "id": "node-inspect-debugger", + "label": "Node Inspect Debugger", + "category": null, + "state": "active" + }, + { + "id": "notion", + "label": "Notion", + "category": null, + "state": "active" + }, + { + "id": "obsidian", + "label": "Obsidian", + "category": null, + "state": "active" + }, + { + "id": "ocr-and-documents", + "label": "Ocr And Documents", + "category": null, + "state": "active" + }, + { + "id": "opencode", + "label": "Opencode", + "category": null, + "state": "active" + }, + { + "id": "openhue", + "label": "Openhue", + "category": null, + "state": "active" + }, + { + "id": "orchestrator", + "label": "Orchestrator", + "category": null, + "state": "active" + }, + { + "id": "p5js", + "label": "P5Js", + "category": null, + "state": "active" + }, + { + "id": "pc-control", + "label": "Pc Control", + "category": null, + "state": "active" + }, + { + "id": "pc-pfad-cache", + "label": "Pc Pfad Cache", + "category": null, + "state": "active" + }, + { + "id": "petdex", + "label": "Petdex", + "category": "productivity", + "state": "active" + }, + { + "id": "polymarket", + "label": "Polymarket", + "category": null, + "state": "active" + }, + { + "id": "popular-web-designs", + "label": "Popular Web Designs", + "category": null, + "state": "active" + }, + { + "id": "powerpoint", + "label": "Powerpoint", + "category": null, + "state": "active" + }, + { + "id": "pretext", + "label": "Pretext", + "category": null, + "state": "active" + }, + { + "id": "python-debugpy", + "label": "Python Debugpy", + "category": null, + "state": "active" + }, + { + "id": "remote-system-access", + "label": "Remote System Access", + "category": null, + "state": "active" + }, + { + "id": "requesting-code-review", + "label": "Requesting Code Review", + "category": null, + "state": "active" + }, + { + "id": "research-paper-writing", + "label": "Research Paper Writing", + "category": "research", + "state": "active" + }, + { + "id": "segment-anything-model", + "label": "Segment Anything Model", + "category": null, + "state": "active" + }, + { + "id": "serving-llms-vllm", + "label": "Serving Llms Vllm", + "category": null, + "state": "active" + }, + { + "id": "session-lifecycle", + "label": "Session Lifecycle", + "category": null, + "state": "active" + }, + { + "id": "simplify-code", + "label": "Simplify Code", + "category": null, + "state": "active" + }, + { + "id": "sketch", + "label": "Sketch", + "category": null, + "state": "active" + }, + { + "id": "songsee", + "label": "Songsee", + "category": null, + "state": "active" + }, + { + "id": "songwriting-and-ai-music", + "label": "Songwriting And Ai Music", + "category": null, + "state": "active" + }, + { + "id": "spike", + "label": "Spike", + "category": null, + "state": "active" + }, + { + "id": "systematic-debugging", + "label": "Systematic Debugging", + "category": null, + "state": "active" + }, + { + "id": "teams-meeting-pipeline", + "label": "Teams Meeting Pipeline", + "category": null, + "state": "active" + }, + { + "id": "test-driven-development", + "label": "Test Driven Development", + "category": null, + "state": "active" + }, + { + "id": "token-optimization", + "label": "Token Optimization", + "category": null, + "state": "active" + }, + { + "id": "touchdesigner-mcp", + "label": "Touchdesigner Mcp", + "category": null, + "state": "active" + }, + { + "id": "user-onboarding", + "label": "User Onboarding", + "category": null, + "state": "active" + }, + { + "id": "wartung", + "label": "Wartung", + "category": null, + "state": "active" + }, + { + "id": "weather-wttr", + "label": "Weather Wttr", + "category": null, + "state": "active" + }, + { + "id": "weights-and-biases", + "label": "Weights And Biases", + "category": null, + "state": "active" + }, + { + "id": "xurl", + "label": "Xurl", + "category": null, + "state": "active" + }, + { + "id": "youtube-content", + "label": "Youtube Content", + "category": null, + "state": "active" + }, + { + "id": "yuanbao", + "label": "Yuanbao", + "category": null, + "state": "active" + } + ], + "edges": [ + { + "skill_a": "orchestrator", + "skill_b": "wartung", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "orchestrator", + "skill_b": "token-optimization", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "wartung", + "skill_b": "token-optimization", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "computer-use", + "skill_b": "dogfood", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "enhanced-chat-ui", + "skill_b": "computer-use", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "enhanced-chat-ui", + "skill_b": "weather-wttr", + "weight": 1.0, + "type": "manual", + "source": "memory" + }, + { + "skill_a": "comfyui", + "skill_b": "humanizer", + "weight": 0.7, + "type": "category", + "source": "category_match" + }, + { + "skill_a": "llm-wiki", + "skill_b": "research-paper-writing", + "weight": 0.7, + "type": "category", + "source": "category_match" + }, + { + "skill_a": "maps", + "skill_b": "petdex", + "weight": 0.7, + "type": "category", + "source": "category_match" + }, + { + "skill_a": "architecture-diagram", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "arxiv", + "skill_b": "ocr-and-documents", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "ascii-art", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-code", + "skill_b": "codex", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-code", + "skill_b": "hermes-agent", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-code", + "skill_b": "opencode", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-design", + "skill_b": "design-md", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-design", + "skill_b": "popular-web-designs", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-design", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "claude-design", + "skill_b": "architecture-diagram", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "codebase-inspection", + "skill_b": "github-repo-management", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "codex", + "skill_b": "hermes-agent", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "design-md", + "skill_b": "popular-web-designs", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "design-md", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "design-md", + "skill_b": "architecture-diagram", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-auth", + "skill_b": "github-pr-workflow", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-auth", + "skill_b": "github-code-review", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-auth", + "skill_b": "github-issues", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-auth", + "skill_b": "github-repo-management", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-code-review", + "skill_b": "github-pr-workflow", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-issues", + "skill_b": "github-pr-workflow", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-repo-management", + "skill_b": "github-pr-workflow", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "github-repo-management", + "skill_b": "github-issues", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "google-workspace", + "skill_b": "himalaya", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "hermes-agent", + "skill_b": "opencode", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "hermes-agent-skill-authoring", + "skill_b": "requesting-code-review", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "humanizer", + "skill_b": "songwriting-and-ai-music", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "llm-wiki", + "skill_b": "obsidian", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "llm-wiki", + "skill_b": "arxiv", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "node-inspect-debugger", + "skill_b": "systematic-debugging", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "node-inspect-debugger", + "skill_b": "python-debugpy", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "opencode", + "skill_b": "codex", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "p5js", + "skill_b": "ascii-video", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "p5js", + "skill_b": "manim-video", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "p5js", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "pretext", + "skill_b": "p5js", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "pretext", + "skill_b": "claude-design", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "pretext", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "pretext", + "skill_b": "architecture-diagram", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "python-debugpy", + "skill_b": "systematic-debugging", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "requesting-code-review", + "skill_b": "test-driven-development", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "requesting-code-review", + "skill_b": "github-code-review", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "research-paper-writing", + "skill_b": "arxiv", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "simplify-code", + "skill_b": "requesting-code-review", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "simplify-code", + "skill_b": "test-driven-development", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "sketch", + "skill_b": "spike", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "sketch", + "skill_b": "claude-design", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "sketch", + "skill_b": "popular-web-designs", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "sketch", + "skill_b": "excalidraw", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "systematic-debugging", + "skill_b": "test-driven-development", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "touchdesigner-mcp", + "skill_b": "ascii-video", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "touchdesigner-mcp", + "skill_b": "manim-video", + "weight": 0.9, + "type": "declared", + "source": "related_declared" + }, + { + "skill_a": "airtable", + "skill_b": "notion", + "weight": 0.5, + "type": "tag_overlap", + "source": "tag_overlap" + }, + { + "skill_a": "design-md", + "skill_b": "sketch", + "weight": 0.5, + "type": "tag_overlap", + "source": "tag_overlap" + }, + { + "skill_a": "github-code-review", + "skill_b": "github-repo-management", + "weight": 0.5, + "type": "tag_overlap", + "source": "tag_overlap" + }, + { + "skill_a": "heartmula", + "skill_b": "songwriting-and-ai-music", + "weight": 0.5, + "type": "tag_overlap", + "source": "tag_overlap" + }, + { + "skill_a": "p5js", + "skill_b": "touchdesigner-mcp", + "weight": 0.5, + "type": "tag_overlap", + "source": "tag_overlap" + } + ] +} \ No newline at end of file diff --git a/deploy/skill-kanten-generator.py b/deploy/skill-kanten-generator.py new file mode 100644 index 0000000..0012d4c --- /dev/null +++ b/deploy/skill-kanten-generator.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +""" +skill-kanten-generator.py + +Generiert Kanten für den Skills-Journey-Graphen basierend auf: +- Kanal A: Manuelle Kanten (aus Memory) +- Kanal B: SKILL.md-Metadaten (gleiche Kategorie, related_skills) +- Kanal C: Co-Nutzung (Session-Analyse, TODO: noch nicht implementiert) + +Ergebnis: journey-graph.json im Werkstatt-Skills-Verzeichnis. +""" + +from __future__ import annotations + +import json +import os +import re +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple + + +# Pfade +HERMES_WORKSPACE = Path.home() / ".hermes" / "profiles" / "werkstatt" +SKILLS_DIR = HERMES_WORKSPACE / "skills" +USAGE_FILE = SKILLS_DIR / ".usage.json" +MEMORY_FILE = HERMES_WORKSPACE / "memories" / "MEMORY.md" +JOURNEY_GRAPH_FILE = SKILLS_DIR / "journey-graph.json" + +# Thresholds für Kanal C (Co-Nutzung) +MIN_CO_USE = 2 +MIN_USE_COUNT = 3 +JACCARD_THRESHOLD = 0.3 + +# Gewichte für Priorisierung +WEIGHT_MANUAL = 1.0 +WEIGHT_RELATED_DECLARED = 0.9 +WEIGHT_CATEGORY_MATCH = 0.7 +WEIGHT_TAG_OVERLAP = 0.5 + + +@dataclass +class Edge: + """Repräsentiert eine Kante im Journey-Graphen.""" + skill_a: str + skill_b: str + weight: float + edge_type: str + source: str + + def key(self) -> Tuple[str, str]: + return (self.skill_a, self.skill_b) if self.skill_a < self.skill_b else (self.skill_b, self.skill_a) + + def to_dict(self) -> Dict: + return { + "skill_a": self.skill_a, + "skill_b": self.skill_b, + "weight": self.weight, + "type": self.edge_type, + "source": self.source, + } + + +@dataclass +class SkillInfo: + """Zusammengeführte Informationen zu einem Skill.""" + name: str + category: Optional[str] = None + related_skills: List[str] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + use_count: int = 0 + state: str = "active" + + +def load_manual_edges_from_memory() -> List[Edge]: + """Liest manuelle Kanten aus dem MEMORY.md-Eintrag 'Skills-Kanten (Journey-Graph)'.""" + edges: List[Edge] = [] + + if not MEMORY_FILE.exists(): + print(f"Warnung: MEMORY.md nicht gefunden unter {MEMORY_FILE}") + return edges + + content = MEMORY_FILE.read_text(encoding="utf-8") + + # Suche nach dem Eintrag "Skills-Kanten (Journey-Graph)" + pattern = r"Skills-Kanten \(Journey-Graph\):\s*(.+?)(?=\n\n|\Z)" + match = re.search(pattern, content, re.DOTALL) + + if not match: + print("Keine manuellen Skills-Kanten im MEMORY.md gefunden.") + return edges + + kanten_text = match.group(1).strip() + + # Parso die Kanten: "skill-a ↔ skill-b (Begründung)" + # Trennzeichen ist " | " für mehrere Kanten + kanten_liste = re.split(r"\s*\|\s*", kanten_text) + + for kante in kanten_liste: + kante = kante.strip() + if not kante: + continue + + # Format: skill-a ↔ skill-b (Begründung) + parts = kante.split("↔") + if len(parts) != 2: + print(f"Warnung: Ungültiges Kanten-Format: {kante}") + continue + + skill_a = parts[0].strip() + skill_b_and_reason = parts[1].strip() + + # Trenne Skill-B und Begründung (in Klammern) + reason_match = re.match(r"(.+?)\s*\((.+?)\)\s*$", skill_b_and_reason) + if reason_match: + skill_b = reason_match.group(1).strip() + # reason = reason_match.group(2).strip() # nicht verwendet, aber verfügbar + else: + skill_b = skill_b_and_reason + print(f"Warnung: Keine Begründung in Kante: {kante}") + + edges.append(Edge( + skill_a=skill_a, + skill_b=skill_b, + weight=WEIGHT_MANUAL, + edge_type="manual", + source="memory" + )) + + print(f"Manuelle Kanten geladen: {len(edges)}") + return edges + + +def build_skill_index() -> Dict[str, SkillInfo]: + """Erstellt einen Index aller Skills aus .usage.json und SKILL.md-Dateien.""" + skills: Dict[str, SkillInfo] = {} + + # 1. Lade .usage.json für use_count und state + if not USAGE_FILE.exists(): + print(f"Warnung: .usage.json nicht gefunden unter {USAGE_FILE}") + return skills + + usage_data = json.loads(USAGE_FILE.read_text(encoding="utf-8")) + + # Extrahiere Skill-Namen aus keys (es gibt auch "Mission Control Model Management" etc.) + for skill_name, info in usage_data.items(): + # Normalisiere Namen (ersetze Leerzeichen, Sonderzeichen für Verzeichnisnamen) + normalized_name = skill_name.lower().replace(" ", "-").replace("_", "-") + + skill = SkillInfo( + name=skill_name, + use_count=info.get("use_count", 0), + state=info.get("state", "active") + ) + skills[normalized_name] = skill + + # 2. Lade SKILL.md Metadaten für Kategorien, Tags, related_skills + # Suche in allen Kategorien-Verzeichnissen + for category_dir in SKILLS_DIR.iterdir(): + if not category_dir.is_dir() or category_dir.name.startswith("."): + continue + + for skill_dir in category_dir.iterdir(): + if not skill_dir.is_dir(): + continue + + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + continue + + content = skill_md.read_text(encoding="utf-8") + + # YAML-Frontmatter extrahieren (alles vor der ersten leeren Zeile nach ---) + frontmatter_match = re.match(r"---\s*(.+?)\s*---", content, re.DOTALL) + if not frontmatter_match: + continue + + frontmatter = frontmatter_match.group(1) + + # Parse YAML-Felder (einfache Regex-basierte Parsers) + category_match = re.search(r"category:\s*(['\"]?)(\w+)\1", frontmatter) + tags_match = re.search(r"tags:\s*\[(.*?)\]", frontmatter, re.DOTALL) + related_match = re.search(r"related_skills:\s*\[(.*?)\]", frontmatter, re.DOTALL) + + # Extrahiere den Skill-Namen aus dem Frontmatter + name_match = re.search(r"name:\s*['\"]?([^\n'\"]+)['\"]?", frontmatter) + if name_match: + skill_name = name_match.group(1).strip().lower().replace(" ", "-") + if skill_name in skills: + if category_match: + skills[skill_name].category = category_match.group(2).lower() + if tags_match: + tags_str = tags_match.group(1) + tags = [t.strip().strip("'\"") for t in tags_str.split(",") if t.strip()] + skills[skill_name].tags.extend(tags) + if related_match: + related_str = related_match.group(1) + related = [r.strip().strip("'\"") for r in related_str.split(",") if r.strip()] + skills[skill_name].related_skills.extend(related) + + print(f"Skill-Index erstellt: {len(skills)} Skills") + return skills + + +def generate_metadata_edges(skills: Dict[str, SkillInfo]) -> List[Edge]: + """Generiert Kanten aus SKILL.md-Metadaten (Kategorie, related_skills, Tags).""" + edges: List[Edge] = [] + skill_list = list(skills.values()) + + # Kategorien und Tags für schnellen Zugriff + category_map: Dict[str, List[str]] = defaultdict(list) + tag_map: Dict[str, List[str]] = defaultdict(list) + + for skill in skill_list: + if skill.category: + category_map[skill.category].append(skill.name) + for tag in skill.tags: + tag_map[tag].append(skill.name) + + # 1. Kategorie-basierte Kanten + for category, skill_names in category_map.items(): + if len(skill_names) < 2: + continue + for i, skill_a in enumerate(skill_names): + for skill_b in skill_names[i + 1:]: + if skill_a != skill_b: + edges.append(Edge( + skill_a=skill_a, + skill_b=skill_b, + weight=WEIGHT_CATEGORY_MATCH, + edge_type="category", + source="category_match" + )) + + # 2. Related skills Kanten (gerichtet, aber als ungerichtete Kante gespeichert) + for skill in skill_list: + for related in skill.related_skills: + if related in skills: + edges.append(Edge( + skill_a=skill.name, + skill_b=related, + weight=WEIGHT_RELATED_DECLARED, + edge_type="declared", + source="related_declared" + )) + + # 3. Tag-Überlappung (mindestens 2 gemeinsame Tags) + for skill_a in skill_list: + for skill_b in skill_list: + if skill_a.name >= skill_b.name: # Vermeide Doppelt-Generierung + continue + + common_tags = set(skill_a.tags) & set(skill_b.tags) + if len(common_tags) >= 2: + edges.append(Edge( + skill_a=skill_a.name, + skill_b=skill_b.name, + weight=WEIGHT_TAG_OVERLAP, + edge_type="tag_overlap", + source="tag_overlap" + )) + + print(f"Metadaten-Kanten generiert: {len(edges)}") + return edges + + +def generate_co_use_edges(skills: Dict[str, SkillInfo]) -> List[Edge]: + """ + Generiert Kanten basierend auf Co-Nutzungsmustern aus Sessions. + HINWEIS: Aktuell nicht implementiert, da Sessions keine Skill-IDs speichern. + Gibt eine leere Liste zurück. + """ + print("Co-Nutzung-Analyse: SKIPPED (Sessions speichern keine Skill-IDs)") + return [] + + +def deduplicate_edges(edges: List[Edge]) -> List[Edge]: + """ + Entfernt doppelte Kanten zwischen denselben Skills. + Wenn mehrere Kanten zwischen selbem Paar existieren, behält die mit höchstem Gewicht. + PRIORITY: manual > declared > category > co_use + """ + # Gruppiere nach Schlüssel (ungereichtes Paar) + edge_groups: Dict[Tuple[str, str], List[Edge]] = defaultdict(list) + for edge in edges: + edge_groups[edge.key()].append(edge) + + # Wähle die beste Kante pro Gruppe + result: List[Edge] = [] + for key, group in edge_groups.items(): + # Sortiere nach Gewicht (höchste zuerst), dann nach Typ-Priorität + type_priority = {"manual": 4, "declared": 3, "category": 2, "tag_overlap": 1, "co_use": 0} + group.sort(key=lambda e: (e.weight, type_priority.get(e.edge_type, -1)), reverse=True) + result.append(group[0]) + + print(f"Nach Deduplizierung: {len(result)} eindeutige Kanten") + return result + + +def generate_journey_graph() -> Dict: + """Generiert den Journey-Graphen mit Knoten und Kanten.""" + # 1. Manuelle Kanten laden + manual_edges = load_manual_edges_from_memory() + + # 2. Skill-Index bauen + skills = build_skill_index() + + # 3. Metadaten-Kanten generieren + metadata_edges = generate_metadata_edges(skills) + + # 4. Co-Nutzung (zurzeit leer) + co_use_edges = generate_co_use_edges(skills) + + # 5. Alle Kanten zusammenfassen + all_edges = manual_edges + metadata_edges + co_use_edges + + # 6. Deduplizieren + final_edges = deduplicate_edges(all_edges) + + # 7. Graph strukturieren + graph = { + "generated_at": None, # Wird unten gesetzt + "version": "1.0.0", + "nodes": [ + { + "id": skill.name, + "label": skill.name.replace("-", " ").title(), + "category": skill.category, + "state": skill.state, + } + for skill in skills.values() if skill.state == "active" + ], + "edges": [edge.to_dict() for edge in final_edges], + } + + # 8. Timestamp hinzufügen + from datetime import datetime, timezone + graph["generated_at"] = datetime.now(timezone.utc).isoformat() + + return graph + + +def main() -> int: + """Hauptfunktion.""" + print("=== Skill-Kanten-Generator ===") + print() + + # Graph generieren + graph = generate_journey_graph() + + # Ausgabe + print() + print(f"Knoten: {len(graph['nodes'])}") + print(f"Kanten: {len(graph['edges'])}") + + # Protokoll + edge_types = defaultdict(int) + for edge in graph["edges"]: + edge_types[edge["type"]] += 1 + print() + print("Kanten nach Typ:") + for t, count in sorted(edge_types.items(), key=lambda x: -x[1]): + print(f" {t}: {count}") + + # Speichern + JOURNEY_GRAPH_FILE.write_text( + json.dumps(graph, indent=2, ensure_ascii=False), + encoding="utf-8" + ) + print() + print(f"Graph gespeichert unter: {JOURNEY_GRAPH_FILE}") + + return 0 + + +if __name__ == "__main__": + import sys + sys.exit(main()) diff --git a/deploy/test_skill_kanten_generator.py b/deploy/test_skill_kanten_generator.py new file mode 100644 index 0000000..70c766b --- /dev/null +++ b/deploy/test_skill_kanten_generator.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +test_skill_kanten_generator.py + +Unit-Tests für skill-kanten-generator.py +""" + +import json +import sys +import tempfile +import unittest +from pathlib import Path + +# Füge das aktuelle Verzeichnis zum Python-Pfad hinzu +sys.path.insert(0, str(Path(__file__).parent)) + +from skill_kanten_generator import ( + Edge, + SkillInfo, + deduplicate_edges, + generate_metadata_edges, + build_skill_index, + generate_journey_graph, +) + + +class TestEdge(unittest.TestCase): + """Tests für die Edge-Datenklasse.""" + + def test_key_ungereicht(self): + """Die key()-Methode sollte ungerichtet sein.""" + edge1 = Edge("a", "b", 0.5, "category", "test") + edge2 = Edge("b", "a", 0.5, "category", "test") + self.assertEqual(edge1.key(), edge2.key()) + + def test_to_dict(self): + """to_dict() sollte das richtige Format liefern.""" + edge = Edge("a", "b", 0.9, "declared", "manual") + result = edge.to_dict() + self.assertEqual(result["skill_a"], "a") + self.assertEqual(result["skill_b"], "b") + self.assertEqual(result["weight"], 0.9) + self.assertEqual(result["type"], "declared") + self.assertEqual(result["source"], "manual") + + +class TestDeduplicateEdges(unittest.TestCase): + """Tests für die Deduplizierung.""" + + def test_keine_doppelt(self): + """Keine Doppelte: gleiche Anzahl zurück.""" + edges = [ + Edge("a", "b", 0.5, "category", "test"), + Edge("c", "d", 0.7, "manual", "test"), + ] + result = deduplicate_edges(edges) + self.assertEqual(len(result), 2) + + def test_doppelt_hoehere_prioritaet(self): + """Bei Doppelt mit gleichen Skills: höhere Priorität gewinnt.""" + edges = [ + Edge("a", "b", 0.5, "category", "test"), # category + Edge("a", "b", 0.9, "declared", "test"), # declared (höher) + ] + result = deduplicate_edges(edges) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].edge_type, "declared") + + def test_doppelt_hoeheres_gewicht(self): + """Bei gleicher Priorität: höheres Gewicht gewinnt.""" + edges = [ + Edge("a", "b", 0.5, "category", "test"), + Edge("a", "b", 0.7, "category", "test"), + ] + result = deduplicate_edges(edges) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].weight, 0.7) + + +class TestBuildSkillIndex(unittest.TestCase): + """Tests für build_skill_index().""" + + def test_leere_index(self): + """Mit leerem usage.json: leerer Index.""" + with tempfile.TemporaryDirectory() as tmpdir: + skills_dir = Path(tmpdir) / "skills" + skills_dir.mkdir() + (skills_dir / ".usage.json").write_text("{}") + + # Mock-Path + import skill_kanten_generator as sg + original = sg.SKILLS_DIR + sg.SKILLS_DIR = skills_dir + sg.USAGE_FILE = skills_dir / ".usage.json" + + try: + index = build_skill_index() + self.assertEqual(len(index), 0) + finally: + sg.SKILLS_DIR = original + sg.USAGE_FILE = original / ".usage.json" + + def test_index_erstellt(self): + """Ein Skill aus .usage.json sollte im Index erscheinen.""" + with tempfile.TemporaryDirectory() as tmpdir: + skills_dir = Path(tmpdir) / "skills" + skills_dir.mkdir() + + usage = { + "my-skill": { + "use_count": 10, + "state": "active", + } + } + (skills_dir / ".usage.json").write_text(json.dumps(usage)) + + import skill_kanten_generator as sg + original = sg.SKILLS_DIR + sg.SKILLS_DIR = skills_dir + sg.USAGE_FILE = skills_dir / ".usage.json" + + try: + index = build_skill_index() + self.assertIn("my-skill", index) + self.assertEqual(index["my-skill"].use_count, 10) + finally: + sg.SKILLS_DIR = original + sg.USAGE_FILE = original / ".usage.json" + + +class TestGenerateMetadataEdges(unittest.TestCase): + """Tests für generate_metadata_edges().""" + + def test_gleiche_kategorie(self): + """Skills mit gleicher Kategorie erhalten eine Kante.""" + skills = { + "skill-a": SkillInfo(name="skill-a", category="test"), + "skill-b": SkillInfo(name="skill-b", category="test"), + } + edges = generate_metadata_edges(skills) + + self.assertEqual(len(edges), 1) + self.assertEqual(edges[0].edge_type, "category") + self.assertEqual(edges[0].weight, 0.7) + + def test_different_categories_no_edge(self): + """Skills mit verschiedenen Kategorien: keine Kante.""" + skills = { + "skill-a": SkillInfo(name="skill-a", category="cat-a"), + "skill-b": SkillInfo(name="skill-b", category="cat-b"), + } + edges = generate_metadata_edges(skills) + self.assertEqual(len(edges), 0) + + def test_related_skills(self): + """Skills mit related_skills-Verbindung erhalten eine Kante.""" + skills = { + "skill-a": SkillInfo( + name="skill-a", + related_skills=["skill-b"], + ), + "skill-b": SkillInfo(name="skill-b"), + } + edges = generate_metadata_edges(skills) + + self.assertEqual(len(edges), 1) + self.assertEqual(edges[0].edge_type, "declared") + self.assertEqual(edges[0].skill_a, "skill-a") + self.assertEqual(edges[0].skill_b, "skill-b") + + +class TestFullWorkflow(unittest.TestCase): + """Integrationstest für den kompletten Workflow.""" + + def test_journey_graph_generated(self): + """Der komplette Workflow sollte einen Graphen erzeugen.""" + with tempfile.TemporaryDirectory() as tmpdir: + hermes_dir = Path(tmpdir) / "hermes" / "profiles" / "werkstatt" + skills_dir = hermes_dir / "skills" + skills_dir.mkdir(parents=True) + + # .usage.json + usage = { + "orchestrator": { + "use_count": 10, + "state": "active", + }, + "wartung": { + "use_count": 7, + "state": "active", + }, + } + (skills_dir / ".usage.json").write_text(json.dumps(usage)) + + # SKILL.md mit gleicher Kategorie + (skills_dir / "orchestrator").mkdir() + (skills_dir / "orchestrator" / "SKILL.md").write_text( + "---\n" + "name: orchestrator\n" + "category: devops\n" + "---\n" + "# Orchestrator\n" + ) + (skills_dir / "wartung").mkdir() + (skills_dir / "wartung" / "SKILL.md").write_text( + "---\n" + "name: wartung\n" + "category: devops\n" + "---\n" + "# Wartung\n" + ) + + import skill_kanten_generator as sg + original = sg.SKILLS_DIR + sg.SKILLS_DIR = skills_dir + sg.USAGE_FILE = skills_dir / ".usage.json" + + try: + graph = generate_journey_graph() + + # Prüfe Struktur + self.assertIn("nodes", graph) + self.assertIn("edges", graph) + self.assertIn("generated_at", graph) + + # Prüfe Knotenanzahl + self.assertEqual(len(graph["nodes"]), 2) + + # Prüfe Kante (beide in gleicher Kategorie) + edge_types = [e["type"] for e in graph["edges"]] + self.assertIn("category", edge_types) + + finally: + sg.SKILLS_DIR = original + sg.USAGE_FILE = original / ".usage.json" + + +if __name__ == "__main__": + unittest.main() -- 2.47.3