skill-kanten-generator: Automatische Skill-Verknüpfung #2
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user