feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a8d79e4484
commit
6a7632146e
490 changed files with 213249 additions and 2 deletions
211
plugins/ms-ai-architect/scripts/export-pdf.py
Executable file
211
plugins/ms-ai-architect/scripts/export-pdf.py
Executable file
|
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a professional PDF from a markdown file.
|
||||
|
||||
Requirements:
|
||||
pip install markdown weasyprint
|
||||
|
||||
Usage:
|
||||
python scripts/export-pdf.py <input.md> [output.pdf]
|
||||
|
||||
If output is not specified, uses the same name as input with .pdf extension.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from weasyprint import HTML
|
||||
|
||||
# --- CSS ---
|
||||
|
||||
CSS = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 25mm 20mm 25mm 20mm;
|
||||
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
font-size: 8pt;
|
||||
color: #718096;
|
||||
}
|
||||
}
|
||||
|
||||
@page :first {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 10.5pt;
|
||||
line-height: 1.6;
|
||||
color: #1a202c;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20pt;
|
||||
font-weight: 700;
|
||||
color: #1a365d;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 15pt;
|
||||
font-weight: 700;
|
||||
color: #1a365d;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 12pt;
|
||||
font-weight: 600;
|
||||
color: #2b6cb0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 10.5pt;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0 20px 0;
|
||||
font-size: 9pt;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
|
||||
th {
|
||||
background-color: #2b6cb0;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
font-size: 8.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td { background-color: #f7fafc; }
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #2b6cb0;
|
||||
margin: 12px 0;
|
||||
padding: 8px 16px;
|
||||
background: #ebf8ff;
|
||||
color: #2a4365;
|
||||
font-size: 10pt;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 9pt;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
ul, ol { margin: 8px 0 12px 0; padding-left: 24px; }
|
||||
li { margin-bottom: 4px; }
|
||||
strong { font-weight: 600; color: #1a202c; }
|
||||
a { color: #2b6cb0; text-decoration: none; }
|
||||
p { margin: 8px 0; }
|
||||
|
||||
.section-break { page-break-before: always; }
|
||||
.score-high { color: #276749; font-weight: 700; }
|
||||
.score-medium { color: #d69e2e; font-weight: 700; }
|
||||
.score-low { color: #c53030; font-weight: 700; }
|
||||
"""
|
||||
|
||||
|
||||
def postprocess_html(html: str) -> str:
|
||||
"""Add CSS classes for scores and risk levels."""
|
||||
# Section breaks on h2 (except first)
|
||||
h2_count = 0
|
||||
|
||||
def add_section_break(match: re.Match) -> str:
|
||||
nonlocal h2_count
|
||||
h2_count += 1
|
||||
if h2_count > 1:
|
||||
return f'<h2 class="section-break">{match.group(1)}</h2>'
|
||||
return match.group(0)
|
||||
|
||||
html = re.sub(r"<h2>(.*?)</h2>", add_section_break, html)
|
||||
|
||||
# Score coloring: 4/5, 5/5 green; 3/5 yellow; 1/5, 2/5 red
|
||||
html = re.sub(r"<td>([45])/5</td>", r'<td><span class="score-high">\1/5</span></td>', html)
|
||||
html = re.sub(r"<td>3/5</td>", '<td><span class="score-medium">3/5</span></td>', html)
|
||||
html = re.sub(r"<td>([12])/5</td>", r'<td><span class="score-low">\1/5</span></td>', html)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python export-pdf.py <input.md> [output.pdf]")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = Path(sys.argv[1])
|
||||
if not input_path.exists():
|
||||
print(f"Error: {input_path} not found")
|
||||
sys.exit(1)
|
||||
|
||||
output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else input_path.with_suffix(".pdf")
|
||||
|
||||
md_text = input_path.read_text(encoding="utf-8")
|
||||
body_html = markdown.markdown(md_text, extensions=["tables", "smarty", "sane_lists"])
|
||||
body_html = postprocess_html(body_html)
|
||||
|
||||
full_html = f"""<!DOCTYPE html>
|
||||
<html lang="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
{body_html}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
HTML(string=full_html).write_pdf(str(output_path))
|
||||
print(f"PDF generated: {output_path}")
|
||||
print(f"Size: {output_path.stat().st_size / 1024:.1f} KB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
235
plugins/ms-ai-architect/scripts/kb-staleness-check.sh
Executable file
235
plugins/ms-ai-architect/scripts/kb-staleness-check.sh
Executable file
|
|
@ -0,0 +1,235 @@
|
|||
#!/bin/bash
|
||||
# kb-staleness-check.sh — Scan knowledge base files for staleness
|
||||
# Usage: bash scripts/kb-staleness-check.sh [--days N] [--priority-only] [--verbose] [--json] [--output FILE]
|
||||
#
|
||||
# Default threshold: 90 days
|
||||
# Priority order: prices > compliance > features > architecture
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# Scan all skill reference directories
|
||||
KB_ROOTS=(
|
||||
"$PLUGIN_ROOT/skills/ms-ai-advisor/references"
|
||||
"$PLUGIN_ROOT/skills/ms-ai-governance/references"
|
||||
"$PLUGIN_ROOT/skills/ms-ai-security/references"
|
||||
"$PLUGIN_ROOT/skills/ms-ai-engineering/references"
|
||||
"$PLUGIN_ROOT/skills/ms-ai-infrastructure/references"
|
||||
)
|
||||
|
||||
# Defaults
|
||||
THRESHOLD_DAYS=90
|
||||
PRIORITY_ONLY=false
|
||||
VERBOSE=false
|
||||
JSON_OUTPUT=false
|
||||
OUTPUT_FILE=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--days)
|
||||
THRESHOLD_DAYS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--priority-only)
|
||||
PRIORITY_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
JSON_OUTPUT=true
|
||||
shift
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: bash scripts/kb-staleness-check.sh [--days N] [--priority-only] [--verbose] [--json] [--output FILE]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for kb_dir in "${KB_ROOTS[@]}"; do
|
||||
if [ ! -d "$kb_dir" ]; then
|
||||
echo "WARNING: Knowledge base directory not found: $kb_dir" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
NOW=$(date +%s)
|
||||
TOTAL=0
|
||||
FRESH=0
|
||||
STALE=0
|
||||
STALE_CRITICAL=0
|
||||
STALE_HIGH=0
|
||||
STALE_MEDIUM=0
|
||||
STALE_LOW=0
|
||||
|
||||
# Collect stale files for sorted summary
|
||||
declare -a STALE_ENTRIES=()
|
||||
|
||||
get_priority() {
|
||||
local filepath="$1"
|
||||
local lower_path
|
||||
lower_path=$(echo "$filepath" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Critical (30 days): cost, pricing, pris
|
||||
if echo "$lower_path" | grep -qE '(cost|pricing|pris)'; then
|
||||
echo "Critical:30"
|
||||
return
|
||||
fi
|
||||
|
||||
# High (60 days): compliance, security, governance
|
||||
if echo "$lower_path" | grep -qE '(responsible-ai|norwegian-public-sector-governance|ai-security-engineering)'; then
|
||||
echo "High:60"
|
||||
return
|
||||
fi
|
||||
|
||||
# Medium (90 days): platforms, features, extensibility
|
||||
if echo "$lower_path" | grep -qE '(platforms|copilot-extensibility|azure-ai-services|multi-modal|performance-scalability|monitoring-observability|agent-orchestration|data-engineering|api-management|hybrid-edge|bcdr|rag-architecture|mlops-genaiops|prompt-engineering)'; then
|
||||
echo "Medium:90"
|
||||
return
|
||||
fi
|
||||
|
||||
# Low (180 days): architecture, development, patterns
|
||||
echo "Low:180"
|
||||
}
|
||||
|
||||
for KB_ROOT in "${KB_ROOTS[@]}"; do
|
||||
[ -d "$KB_ROOT" ] || continue
|
||||
while IFS= read -r -d '' file; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
|
||||
# macOS-compatible stat for modification time
|
||||
MOD_EPOCH=$(stat -f '%m' "$file" 2>/dev/null || stat -c '%Y' "$file" 2>/dev/null)
|
||||
DAYS_OLD=$(( (NOW - MOD_EPOCH) / 86400 ))
|
||||
|
||||
REL_PATH="${file#"$KB_ROOT/"}"
|
||||
PRIORITY_INFO=$(get_priority "$REL_PATH")
|
||||
PRIORITY="${PRIORITY_INFO%%:*}"
|
||||
PRIORITY_THRESHOLD="${PRIORITY_INFO##*:}"
|
||||
|
||||
if [ "$DAYS_OLD" -gt "$PRIORITY_THRESHOLD" ]; then
|
||||
STALE=$((STALE + 1))
|
||||
case "$PRIORITY" in
|
||||
Critical) STALE_CRITICAL=$((STALE_CRITICAL + 1)) ;;
|
||||
High) STALE_HIGH=$((STALE_HIGH + 1)) ;;
|
||||
Medium) STALE_MEDIUM=$((STALE_MEDIUM + 1)) ;;
|
||||
Low) STALE_LOW=$((STALE_LOW + 1)) ;;
|
||||
esac
|
||||
|
||||
FULL_REL="${file#"$PLUGIN_ROOT/"}"
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo "[STALE] $REL_PATH — ${DAYS_OLD} days old (threshold: ${PRIORITY_THRESHOLD}) — Priority: $PRIORITY" >&2
|
||||
else
|
||||
echo "[STALE] $REL_PATH — ${DAYS_OLD} days old (threshold: ${PRIORITY_THRESHOLD}) — Priority: $PRIORITY"
|
||||
fi
|
||||
STALE_ENTRIES+=("${DAYS_OLD}:${PRIORITY}:${FULL_REL}")
|
||||
else
|
||||
FRESH=$((FRESH + 1))
|
||||
if [ "$VERBOSE" = true ] && [ "$PRIORITY_ONLY" = false ]; then
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo "[FRESH] $REL_PATH — ${DAYS_OLD} days old (threshold: ${PRIORITY_THRESHOLD}) — Priority: $PRIORITY" >&2
|
||||
else
|
||||
echo "[FRESH] $REL_PATH — ${DAYS_OLD} days old (threshold: ${PRIORITY_THRESHOLD}) — Priority: $PRIORITY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < <(find "$KB_ROOT" -name '*.md' -type f -print0)
|
||||
done
|
||||
|
||||
# JSON output mode
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
JSON="{"
|
||||
JSON+="\"generated_at\":\"$(date -Iseconds)\","
|
||||
JSON+="\"total\":$TOTAL,"
|
||||
JSON+="\"fresh\":$FRESH,"
|
||||
JSON+="\"stale\":$STALE,"
|
||||
JSON+="\"stale_by_priority\":{\"critical\":$STALE_CRITICAL,\"high\":$STALE_HIGH,\"medium\":$STALE_MEDIUM,\"low\":$STALE_LOW},"
|
||||
JSON+="\"files\":["
|
||||
|
||||
FIRST=true
|
||||
for entry in "${STALE_ENTRIES[@]}"; do
|
||||
days="${entry%%:*}"
|
||||
rest="${entry#*:}"
|
||||
priority="${rest%%:*}"
|
||||
filepath="${rest#*:}"
|
||||
|
||||
# Determine skill from path
|
||||
skill="unknown"
|
||||
case "$filepath" in
|
||||
*ms-ai-advisor*) skill="ms-ai-advisor" ;;
|
||||
*ms-ai-engineering*) skill="ms-ai-engineering" ;;
|
||||
*ms-ai-governance*) skill="ms-ai-governance" ;;
|
||||
*ms-ai-security*) skill="ms-ai-security" ;;
|
||||
*ms-ai-infrastructure*) skill="ms-ai-infrastructure" ;;
|
||||
esac
|
||||
|
||||
# Determine category from path
|
||||
category=$(echo "$filepath" | sed -E 's|.*/references/([^/]+)/.*|\1|')
|
||||
|
||||
if [ "$FIRST" = true ]; then
|
||||
FIRST=false
|
||||
else
|
||||
JSON+=","
|
||||
fi
|
||||
JSON+="{\"path\":\"$filepath\",\"skill\":\"$skill\",\"category\":\"$category\",\"age_days\":$days,\"priority\":\"$priority\"}"
|
||||
done
|
||||
|
||||
JSON+="]}"
|
||||
|
||||
if [ -n "$OUTPUT_FILE" ]; then
|
||||
echo "$JSON" > "$OUTPUT_FILE"
|
||||
echo "JSON written to: $OUTPUT_FILE" >&2
|
||||
else
|
||||
echo "$JSON"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== KB Freshness Report ==="
|
||||
echo "Total files: $TOTAL"
|
||||
echo "Fresh: $FRESH"
|
||||
echo "Stale: $STALE (Critical: $STALE_CRITICAL, High: $STALE_HIGH, Medium: $STALE_MEDIUM, Low: $STALE_LOW)"
|
||||
|
||||
if [ "$STALE" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Recommended update order:"
|
||||
|
||||
# Sort stale entries: Critical first, then High, Medium, Low; within priority by age descending
|
||||
PRIORITY_ORDER="Critical High Medium Low"
|
||||
INDEX=1
|
||||
|
||||
for prio in $PRIORITY_ORDER; do
|
||||
# Collect entries for this priority, sort by age descending
|
||||
PRIO_ENTRIES=()
|
||||
for entry in "${STALE_ENTRIES[@]}"; do
|
||||
entry_prio="${entry#*:}"
|
||||
entry_prio="${entry_prio%%:*}"
|
||||
if [ "$entry_prio" = "$prio" ]; then
|
||||
PRIO_ENTRIES+=("$entry")
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort by days (first field) descending
|
||||
if [ ${#PRIO_ENTRIES[@]} -gt 0 ]; then
|
||||
SORTED=$(printf '%s\n' "${PRIO_ENTRIES[@]}" | sort -t: -k1 -nr)
|
||||
while IFS= read -r sorted_entry; do
|
||||
days="${sorted_entry%%:*}"
|
||||
rest="${sorted_entry#*:}"
|
||||
rest="${rest#*:}"
|
||||
echo " ${INDEX}. [$prio] $rest (${days} days)"
|
||||
INDEX=$((INDEX + 1))
|
||||
done <<< "$SORTED"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Run with --verbose to see fresh files. Use --days N to override threshold. Use --json for machine-readable output."
|
||||
406
plugins/ms-ai-architect/scripts/skill-gen/categories.json
Normal file
406
plugins/ms-ai-architect/scripts/skill-gen/categories.json
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"created": "2026-02-03",
|
||||
"target_dir": "skills/ms-ai-engineering/references",
|
||||
"total_estimated_skills": "300-350",
|
||||
"waves": [
|
||||
{
|
||||
"wave": 1,
|
||||
"priority": "HIGH",
|
||||
"description": "Kritisk manglende kunnskap for enterprise AI-arkitektur",
|
||||
"categories": [
|
||||
"azure-ai-services",
|
||||
"rag-architecture",
|
||||
"responsible-ai",
|
||||
"copilot-extensibility",
|
||||
"prompt-engineering",
|
||||
"cost-optimization",
|
||||
"mlops-genaiops"
|
||||
]
|
||||
},
|
||||
{
|
||||
"wave": 1.5,
|
||||
"priority": "HIGH",
|
||||
"description": "Utredningsstøtte: norsk offentlig sektor, AI-sikkerhet og observerbarhet",
|
||||
"categories": [
|
||||
"norwegian-public-sector-governance",
|
||||
"ai-security-engineering",
|
||||
"monitoring-observability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"wave": 2,
|
||||
"priority": "MEDIUM",
|
||||
"description": "Verdifulle tillegg for komplett arkitekturdekning",
|
||||
"categories": [
|
||||
"agent-orchestration",
|
||||
"bcdr",
|
||||
"data-engineering",
|
||||
"api-management",
|
||||
"hybrid-edge",
|
||||
"multi-modal",
|
||||
"performance-scalability"
|
||||
]
|
||||
}
|
||||
],
|
||||
"categories": {
|
||||
"azure-ai-services": {
|
||||
"name": "Azure AI Services (Foundry Tools)",
|
||||
"dir": "azure-ai-services",
|
||||
"priority": "HIGH",
|
||||
"description": "Pre-bygde AI-tjenester: Vision, Speech, Language, Document Intelligence, Translator, Content Understanding. Fundamentale byggeblokker for enterprise AI.",
|
||||
"estimated_skills": 20,
|
||||
"examples": [
|
||||
"azure-ai-vision-overview",
|
||||
"document-intelligence-models",
|
||||
"speech-services-architecture",
|
||||
"language-services-text-analytics",
|
||||
"content-understanding-multimodal",
|
||||
"translator-custom-models",
|
||||
"azure-ai-search-indexing",
|
||||
"custom-vision-vs-florence",
|
||||
"ai-services-networking-security",
|
||||
"ai-services-pricing-optimization"
|
||||
],
|
||||
"existing_overlap": ["platforms/azure-ai-foundry.md"],
|
||||
"notes": "Foundry Tools er ny branding (2025). Unngå duplikering med azure-ai-foundry.md som dekker overordnet plattform."
|
||||
},
|
||||
"rag-architecture": {
|
||||
"name": "RAG Architecture & Semantic Search",
|
||||
"dir": "rag-architecture",
|
||||
"priority": "HIGH",
|
||||
"description": "Retrieval-Augmented Generation med Azure AI Search. Vektorindeksering, embedding, hybrid search, reranking, chunking, citation tracking.",
|
||||
"estimated_skills": 22,
|
||||
"examples": [
|
||||
"rag-architecture-patterns",
|
||||
"azure-ai-search-vector-indexing",
|
||||
"embedding-model-selection",
|
||||
"chunking-strategies",
|
||||
"hybrid-search-configuration",
|
||||
"semantic-ranker-optimization",
|
||||
"rag-evaluation-metrics",
|
||||
"multi-index-federation",
|
||||
"rag-security-rbac",
|
||||
"graphrag-knowledge-graphs"
|
||||
],
|
||||
"existing_overlap": ["architecture/decision-trees.md"],
|
||||
"notes": "RAG er det vanligste mønsteret for enterprise AI. Detaljer er planlagt som ms-rag-architect plugin men grunnleggende arkitektur dekkes her."
|
||||
},
|
||||
"responsible-ai": {
|
||||
"name": "Responsible AI & Governance",
|
||||
"dir": "responsible-ai",
|
||||
"priority": "HIGH",
|
||||
"description": "Microsofts Responsible AI-rammeverk, AI-etikk, bias-deteksjon, forklarbarhet, GDPR/AI Act compliance, AI governance for offentlig sektor.",
|
||||
"estimated_skills": 22,
|
||||
"examples": [
|
||||
"responsible-ai-framework-overview",
|
||||
"ai-act-compliance-guide",
|
||||
"bias-detection-mitigation",
|
||||
"model-explainability-techniques",
|
||||
"ai-governance-structure",
|
||||
"ai-center-of-excellence",
|
||||
"red-teaming-ai-models",
|
||||
"content-safety-implementation",
|
||||
"ai-impact-assessment",
|
||||
"transparency-documentation"
|
||||
],
|
||||
"existing_overlap": ["architecture/security.md", "architecture/public-sector-checklist.md"],
|
||||
"notes": "Utfyller security.md (teknisk sikkerhet) med governance og compliance. Spesielt viktig for offentlig sektor etter AI Act."
|
||||
},
|
||||
"copilot-extensibility": {
|
||||
"name": "Copilot Extensibility & Integration",
|
||||
"dir": "copilot-extensibility",
|
||||
"priority": "HIGH",
|
||||
"description": "Utvidelse av M365 Copilot og Copilot Studio: declarative agents, custom engine agents, plugins, connectors, Graph API, MCP.",
|
||||
"estimated_skills": 22,
|
||||
"examples": [
|
||||
"declarative-agents-overview",
|
||||
"custom-engine-agents",
|
||||
"copilot-studio-topics-entities",
|
||||
"graph-api-for-copilot",
|
||||
"copilot-connectors-patterns",
|
||||
"mcp-integration-copilot-studio",
|
||||
"copilot-analytics-usage",
|
||||
"teams-copilot-extensions",
|
||||
"sharepoint-agents",
|
||||
"copilot-studio-dlp-governance"
|
||||
],
|
||||
"existing_overlap": ["platforms/copilot-studio.md", "platforms/m365-copilot.md"],
|
||||
"notes": "Går dypere enn eksisterende plattformfiler. Fokus på implementeringsmønstre, ikke overordnet arkitektur."
|
||||
},
|
||||
"prompt-engineering": {
|
||||
"name": "Prompt Engineering & LLM Optimization",
|
||||
"dir": "prompt-engineering",
|
||||
"priority": "HIGH",
|
||||
"description": "System message design, few-shot/zero-shot teknikker, chain-of-thought, reasoning-modeller (O1/O3), grounding, output-formatering.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"system-message-design-patterns",
|
||||
"few-shot-learning-techniques",
|
||||
"chain-of-thought-prompting",
|
||||
"reasoning-models-o1-o3",
|
||||
"structured-output-json-mode",
|
||||
"function-calling-patterns",
|
||||
"grounding-with-search",
|
||||
"temperature-and-sampling",
|
||||
"token-optimization-techniques",
|
||||
"prompt-testing-evaluation"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Helt nytt område. Direkte påvirkning på kvaliteten av alle AI-løsninger."
|
||||
},
|
||||
"cost-optimization": {
|
||||
"name": "Cost Optimization & FinOps for AI",
|
||||
"dir": "cost-optimization",
|
||||
"priority": "HIGH",
|
||||
"description": "Token-optimalisering, caching, reserved capacity, modellvalg, Azure Cost Management, chargeback, budsjettplanlegging for AI.",
|
||||
"estimated_skills": 20,
|
||||
"examples": [
|
||||
"token-cost-optimization",
|
||||
"semantic-caching-patterns",
|
||||
"reserved-capacity-planning",
|
||||
"model-selection-price-performance",
|
||||
"azure-cost-management-ai",
|
||||
"ptu-vs-paygo-decision",
|
||||
"ai-builder-credits-transition",
|
||||
"cost-allocation-chargeback",
|
||||
"budget-forecasting-ai",
|
||||
"small-language-models-cost"
|
||||
],
|
||||
"existing_overlap": ["architecture/cost-models.md"],
|
||||
"notes": "Utfyller cost-models.md med dypere strategier. cost-models.md dekker prislister, dette dekker optimaliseringsteknikker."
|
||||
},
|
||||
"mlops-genaiops": {
|
||||
"name": "MLOps & GenAIOps",
|
||||
"dir": "mlops-genaiops",
|
||||
"priority": "HIGH",
|
||||
"description": "CI/CD for AI, modellmonitorering, versjonshåndtering, A/B-testing, retraining, evaluering, Azure ML pipelines for produksjon.",
|
||||
"estimated_skills": 22,
|
||||
"examples": [
|
||||
"genaiops-overview",
|
||||
"azure-ml-pipelines",
|
||||
"model-versioning-registry",
|
||||
"llm-evaluation-framework",
|
||||
"ab-testing-ai-models",
|
||||
"data-drift-monitoring",
|
||||
"automated-retraining",
|
||||
"ci-cd-ai-models",
|
||||
"prompt-flow-production",
|
||||
"model-deployment-strategies"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Helt nytt område. Kritisk for å gå fra prototyp til produksjon."
|
||||
},
|
||||
"data-engineering": {
|
||||
"name": "Data Engineering for AI",
|
||||
"dir": "data-engineering",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Dataintegrasjon med Microsoft Fabric, Data Factory, OneLake, Databricks. Zero-ETL, lakehouse-arkitektur, AI-drevet dataintegrering.",
|
||||
"estimated_skills": 22,
|
||||
"examples": [
|
||||
"microsoft-fabric-for-ai",
|
||||
"onelake-data-strategy",
|
||||
"data-factory-ai-pipelines",
|
||||
"zero-etl-patterns",
|
||||
"data-quality-for-ai",
|
||||
"real-time-streaming-ai",
|
||||
"dataverse-ai-integration",
|
||||
"data-lakehouse-architecture",
|
||||
"data-governance-purview",
|
||||
"synthetic-data-generation"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Datakvalitet er #1 årsak til AI-prosjektfeil. Microsoft Fabric er raskt voksende."
|
||||
},
|
||||
"api-management": {
|
||||
"name": "API Management & AI Gateway",
|
||||
"dir": "api-management",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Azure API Management som AI-gateway: rate limiting, token quota, load balancing, circuit breaker, autentisering, multi-region.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"apim-ai-gateway-overview",
|
||||
"token-rate-limiting",
|
||||
"load-balancing-openai",
|
||||
"circuit-breaker-patterns",
|
||||
"multi-region-gateway",
|
||||
"apim-authentication-patterns",
|
||||
"backend-pool-management",
|
||||
"streaming-support-apim",
|
||||
"cost-tracking-apim",
|
||||
"apim-vs-direct-access"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Viktig for enterprise-skalering. APIM AI Gateway er relativt nytt (2024-2025)."
|
||||
},
|
||||
"hybrid-edge": {
|
||||
"name": "Hybrid Cloud & Edge AI",
|
||||
"dir": "hybrid-edge",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Azure Arc, Azure Local, IoT Operations, edge AI inferencing, disconnected scenarios, datasuverenitet for offentlig sektor.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"azure-arc-ai-management",
|
||||
"azure-local-ai-workloads",
|
||||
"edge-ai-inferencing",
|
||||
"disconnected-ai-scenarios",
|
||||
"data-sovereignty-patterns",
|
||||
"iot-operations-ai",
|
||||
"hybrid-rag-architecture",
|
||||
"on-premises-llm-deployment",
|
||||
"azure-confidential-computing",
|
||||
"sovereign-cloud-norway"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Spesielt relevant for norsk offentlig sektor med suverenitetskrav og sikkerhetsgradert informasjon."
|
||||
},
|
||||
"bcdr": {
|
||||
"name": "Business Continuity & Disaster Recovery",
|
||||
"dir": "bcdr",
|
||||
"priority": "MEDIUM",
|
||||
"description": "HA, DR og BCDR for AI: multi-region, backup, failover, RTO/RPO for Azure OpenAI og AI Foundry.",
|
||||
"estimated_skills": 16,
|
||||
"examples": [
|
||||
"multi-region-azure-openai",
|
||||
"ai-foundry-dr-planning",
|
||||
"backup-recovery-strategies",
|
||||
"failover-testing-ai",
|
||||
"rto-rpo-ai-services",
|
||||
"data-replication-patterns",
|
||||
"geo-redundancy-search",
|
||||
"incident-response-ai",
|
||||
"capacity-planning-dr",
|
||||
"compliance-bcdr-requirements"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Nødvendig for kritiske produksjonssystemer i offentlig sektor."
|
||||
},
|
||||
"multi-modal": {
|
||||
"name": "Multi-Modal AI",
|
||||
"dir": "multi-modal",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Tekst + bilde + tale + video: GPT-4V/GPT-5 vision, Video Indexer, Speech-integrasjon, multi-modal RAG, aksessibilitet.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"gpt-vision-architecture",
|
||||
"video-indexer-ai",
|
||||
"multi-modal-rag",
|
||||
"speech-to-ai-pipelines",
|
||||
"image-generation-dall-e",
|
||||
"document-vision-processing",
|
||||
"accessibility-multi-modal",
|
||||
"real-time-audio-api",
|
||||
"video-analysis-patterns",
|
||||
"multi-modal-evaluation"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Økende etterspørsel etter multi-modale løsninger. GPT-5 styrker vision-kapabiliteter."
|
||||
},
|
||||
"agent-orchestration": {
|
||||
"name": "Agent Orchestration & Automation",
|
||||
"dir": "agent-orchestration",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Multi-agent systemer, orkesteringsmønstre, agent-kommunikasjon, Agent 365, Semantic Kernel/Agent Framework-mønstre.",
|
||||
"estimated_skills": 20,
|
||||
"examples": [
|
||||
"multi-agent-patterns",
|
||||
"agent-orchestration-topologies",
|
||||
"agent-to-agent-communication",
|
||||
"agent-365-governance",
|
||||
"semantic-kernel-agents",
|
||||
"agent-memory-patterns",
|
||||
"tool-use-patterns",
|
||||
"agent-evaluation-testing",
|
||||
"human-in-the-loop-agents",
|
||||
"autonomous-workflow-patterns"
|
||||
],
|
||||
"existing_overlap": ["development/agent-framework.md"],
|
||||
"notes": "Utfyller agent-framework.md med orkestrerings- og designmønstre."
|
||||
},
|
||||
"performance-scalability": {
|
||||
"name": "Performance & Scalability",
|
||||
"dir": "performance-scalability",
|
||||
"priority": "MEDIUM",
|
||||
"description": "Latency-reduksjon, throughput, caching, batching, streaming, auto-scaling, CDN for AI-workloads.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"latency-optimization-openai",
|
||||
"streaming-responses-patterns",
|
||||
"batch-api-usage",
|
||||
"auto-scaling-ai-infra",
|
||||
"cdn-edge-caching-ai",
|
||||
"connection-pooling-patterns",
|
||||
"throughput-optimization",
|
||||
"model-distillation-perf",
|
||||
"async-processing-patterns",
|
||||
"load-testing-ai-services"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Viktig for brukeropplevelse. Komplementerer cost-optimization."
|
||||
},
|
||||
"monitoring-observability": {
|
||||
"name": "Monitoring & Observability",
|
||||
"dir": "monitoring-observability",
|
||||
"priority": "HIGH",
|
||||
"description": "Azure Monitor, Application Insights, Log Analytics for AI. Token tracking, anomaly detection, dashboards, alerting.",
|
||||
"estimated_skills": 18,
|
||||
"examples": [
|
||||
"azure-monitor-ai-workloads",
|
||||
"application-insights-llm",
|
||||
"token-usage-tracking",
|
||||
"anomaly-detection-ai",
|
||||
"custom-ai-dashboards",
|
||||
"alerting-strategies-ai",
|
||||
"distributed-tracing-ai",
|
||||
"log-analytics-ai-queries",
|
||||
"sla-monitoring-ai",
|
||||
"cost-attribution-monitoring"
|
||||
],
|
||||
"existing_overlap": [],
|
||||
"notes": "Nødvendig for produksjonsoperasjoner. Komplementerer MLOps."
|
||||
},
|
||||
"norwegian-public-sector-governance": {
|
||||
"name": "Norwegian Public Sector AI Governance",
|
||||
"dir": "norwegian-public-sector-governance",
|
||||
"priority": "HIGH",
|
||||
"description": "Norsk lovverk, Digdir-rammeverk og forvaltningsmetodikk anvendt på AI. Utredningsinstruksen, Digdirs 7 arkitekturprinsipper, rammeverk for digital samhandling (EIF), DPIA, ROS-analyse, NSM grunnprinsipper, anskaffelser og gevinstrealisering for AI i offentlig sektor.",
|
||||
"estimated_skills": 20,
|
||||
"research_sources": ["websearch", "regjeringen.no", "lovdata.no", "digdir.no", "nsm.no", "datatilsynet.no"],
|
||||
"examples": [
|
||||
"utredningsinstruksen-methodology",
|
||||
"forvaltningsloven-ai-decisions",
|
||||
"digdir-principle-1-user-centric",
|
||||
"digdir-principle-4-trust",
|
||||
"digital-samhandling-5-layers",
|
||||
"dpia-norwegian-methodology",
|
||||
"ros-analyse-ai-systems",
|
||||
"nsm-grunnprinsipper-ai-mapping",
|
||||
"anskaffelser-ai-procurement",
|
||||
"gevinstrealisering-ai-projects"
|
||||
],
|
||||
"existing_overlap": ["architecture/public-sector-checklist.md", "architecture/ai-utredning-template.md"],
|
||||
"notes": "Fundamentalt annerledes enn øvrige kategorier: primærkilder er regjeringen.no, lovdata.no, digdir.no, nsm.no — IKKE microsoft-learn. Innhold er regulatorisk/juridisk, ikke teknisk produktdokumentasjon. Prompt-template må bruke WebSearch for norske kilder i tillegg til microsoft-learn MCP."
|
||||
},
|
||||
"ai-security-engineering": {
|
||||
"name": "AI Security Engineering",
|
||||
"dir": "ai-security-engineering",
|
||||
"priority": "HIGH",
|
||||
"description": "Operasjonell AI-sikkerhet: prompt injection forsvar, jailbreak-prevention, content safety kalibrering, PII-deteksjon, trusselmodellering, sikkerhetsscoring, hendelseshåndtering, output-validering, zero trust for AI, datalekkasjeforebygging og red teaming.",
|
||||
"estimated_skills": 15,
|
||||
"examples": [
|
||||
"prompt-injection-defense-patterns",
|
||||
"jailbreak-prevention-production",
|
||||
"content-safety-filter-calibration",
|
||||
"pii-detection-norwegian-text",
|
||||
"ai-threat-modeling-stride",
|
||||
"security-scoring-rubric-6dimensions",
|
||||
"ai-incident-response-procedures",
|
||||
"output-validation-grounding-verification",
|
||||
"zero-trust-ai-services",
|
||||
"ai-red-team-operations-practical"
|
||||
],
|
||||
"existing_overlap": ["architecture/security.md", "responsible-ai/red-teaming-ai-models.md", "responsible-ai/content-safety-implementation.md", "prompt-engineering/adversarial-prompting-and-jailbreaks.md"],
|
||||
"notes": "Komplementerer responsible-ai (governance/teori) og prompt-engineering (angrepsteknikker) med OPERASJONELLE forsvarsmønstre. Fokus: forsvar, deteksjon, respons — ikke policy eller angrep."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"description": "Maps KB categories to their target skill directories",
|
||||
"mapping": {
|
||||
"rag-architecture": "ms-ai-engineering",
|
||||
"azure-ai-services": "ms-ai-engineering",
|
||||
"copilot-extensibility": "ms-ai-engineering",
|
||||
"prompt-engineering": "ms-ai-engineering",
|
||||
"data-engineering": "ms-ai-engineering",
|
||||
"api-management": "ms-ai-engineering",
|
||||
"agent-orchestration": "ms-ai-engineering",
|
||||
"multi-modal": "ms-ai-engineering",
|
||||
"mlops-genaiops": "ms-ai-engineering",
|
||||
"performance-scalability": "ms-ai-engineering",
|
||||
"monitoring-observability": "ms-ai-engineering",
|
||||
"responsible-ai": "ms-ai-governance",
|
||||
"norwegian-public-sector-governance": "ms-ai-governance",
|
||||
"cost-optimization": "ms-ai-security",
|
||||
"ai-security-engineering": "ms-ai-security",
|
||||
"security-scoring": "ms-ai-security",
|
||||
"hybrid-edge": "ms-ai-infrastructure",
|
||||
"bcdr": "ms-ai-infrastructure",
|
||||
"platforms": "ms-ai-advisor",
|
||||
"architecture": "ms-ai-advisor"
|
||||
},
|
||||
"priority_thresholds": {
|
||||
"critical": 30,
|
||||
"high": 60,
|
||||
"medium": 90,
|
||||
"low": 180
|
||||
}
|
||||
}
|
||||
301
plugins/ms-ai-architect/scripts/skill-gen/expand-categories.sh
Executable file
301
plugins/ms-ai-architect/scripts/skill-gen/expand-categories.sh
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
#!/bin/bash
|
||||
# expand-categories.sh — Expand skill categories into full manifest
|
||||
#
|
||||
# Uses claude --print to expand each category in categories.json
|
||||
# into 15-25 individual skill titles, producing manifest.json
|
||||
#
|
||||
# Usage:
|
||||
# ./expand-categories.sh # Expand all categories
|
||||
# ./expand-categories.sh azure-ai-services # Expand single category
|
||||
# ./expand-categories.sh --wave 1 # Expand wave 1 only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - claude CLI installed and authenticated
|
||||
# - jq installed
|
||||
# - categories.json in same directory
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
CATEGORIES_FILE="$SCRIPT_DIR/categories.json"
|
||||
MANIFEST_FILE="$SCRIPT_DIR/manifest.json"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
|
||||
# Model for expansion (haiku is sufficient for generating titles)
|
||||
MODEL="${MODEL:-haiku}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[expand]${NC} $1" >&2; }
|
||||
success() { echo -e "${GREEN}[expand]${NC} $1" >&2; }
|
||||
warn() { echo -e "${YELLOW}[expand]${NC} $1" >&2; }
|
||||
error() { echo -e "${RED}[expand]${NC} $1" >&2; }
|
||||
|
||||
# Check prerequisites
|
||||
check_prereqs() {
|
||||
if ! command -v claude &>/dev/null; then
|
||||
error "claude CLI not found. Install: npm install -g @anthropic-ai/claude-code"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq &>/dev/null; then
|
||||
error "jq not found. Install: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$CATEGORIES_FILE" ]]; then
|
||||
error "categories.json not found at $CATEGORIES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get list of existing reference files for context
|
||||
get_existing_refs() {
|
||||
local category_dir="$1"
|
||||
local refs_dir="$PLUGIN_DIR/skills/ms-ai-engineering/references"
|
||||
|
||||
# List all existing reference files
|
||||
find "$refs_dir" -name "*.md" -type f 2>/dev/null | while read -r f; do
|
||||
basename "$f" .md
|
||||
done | sort | tr '\n' ', '
|
||||
}
|
||||
|
||||
# Expand a single category
|
||||
expand_category() {
|
||||
local category_key="$1"
|
||||
|
||||
local name description estimated examples existing_overlap notes
|
||||
name=$(jq -r ".categories[\"$category_key\"].name" "$CATEGORIES_FILE")
|
||||
description=$(jq -r ".categories[\"$category_key\"].description" "$CATEGORIES_FILE")
|
||||
estimated=$(jq -r ".categories[\"$category_key\"].estimated_skills" "$CATEGORIES_FILE")
|
||||
examples=$(jq -r ".categories[\"$category_key\"].examples | join(\", \")" "$CATEGORIES_FILE")
|
||||
existing_overlap=$(jq -r ".categories[\"$category_key\"].existing_overlap | join(\", \")" "$CATEGORIES_FILE")
|
||||
notes=$(jq -r ".categories[\"$category_key\"].notes" "$CATEGORIES_FILE")
|
||||
|
||||
local existing_refs
|
||||
existing_refs=$(get_existing_refs "$category_key")
|
||||
|
||||
log "Expanding: $name ($estimated skills estimated)"
|
||||
|
||||
local prompt
|
||||
prompt="Du er en Microsoft AI Solution Architect som planlegger en kunnskapsbase.
|
||||
|
||||
Kategorien **${name}** trenger individuelle kunnskapsfiler (skills).
|
||||
|
||||
## Kategori-beskrivelse
|
||||
${description}
|
||||
|
||||
## Eksisterende filer i kunnskapsbasen (unngå duplikering)
|
||||
${existing_refs}
|
||||
|
||||
## Eksisterende overlapp å ta hensyn til
|
||||
${existing_overlap}
|
||||
|
||||
## Eksempel-titler (for inspirasjon, ikke begrens deg til disse)
|
||||
${examples}
|
||||
|
||||
## Notater
|
||||
${notes}
|
||||
|
||||
## Oppgave
|
||||
|
||||
Generer en JSON-array med NØYAKTIG ${estimated} skills for denne kategorien.
|
||||
|
||||
Hver skill skal ha:
|
||||
- \`id\`: kebab-case filnavn (uten .md)
|
||||
- \`title\`: Engelsk tittel (kortfattet, beskrivende)
|
||||
- \`description\`: 1-2 setninger på norsk om hva filen dekker
|
||||
- \`subtopics\`: 3-5 viktige undertemaer som array
|
||||
|
||||
Regler:
|
||||
1. Ikke dupliser emner som allerede finnes i eksisterende filer
|
||||
2. Sørg for bred dekning uten overlapp mellom skills
|
||||
3. Titler skal være spesifikke (\"Azure AI Vision OCR and Document Processing\", ikke bare \"Vision\")
|
||||
4. Prioriter mest nyttige emner for enterprise AI-arkitekter i norsk offentlig sektor
|
||||
5. Returner KUN valid JSON-array, ingen annen tekst
|
||||
|
||||
Eksempel-format:
|
||||
[
|
||||
{
|
||||
\"id\": \"example-skill-name\",
|
||||
\"title\": \"Example Skill - Full Descriptive Title\",
|
||||
\"description\": \"Beskrivelse av hva denne kunnskapsfilen dekker.\",
|
||||
\"subtopics\": [\"subtopic-1\", \"subtopic-2\", \"subtopic-3\"]
|
||||
}
|
||||
]"
|
||||
|
||||
local output
|
||||
output=$(claude --print --model "$MODEL" "$prompt" 2>"$LOG_DIR/expand-${category_key}.err")
|
||||
|
||||
# Extract JSON array from response (handles markdown code blocks, plain JSON, etc.)
|
||||
local json_output
|
||||
json_output=$(python3 -c "
|
||||
import sys, json, re
|
||||
text = sys.stdin.read()
|
||||
# Try to find JSON array in code blocks first
|
||||
m = re.search(r'\`\`\`(?:json)?\s*(\[[\s\S]*?\])\s*\`\`\`', text)
|
||||
if m:
|
||||
arr = json.loads(m.group(1))
|
||||
print(json.dumps(arr))
|
||||
sys.exit(0)
|
||||
# Try to find bare JSON array
|
||||
m = re.search(r'(\[[\s\S]*\])', text)
|
||||
if m:
|
||||
try:
|
||||
arr = json.loads(m.group(1))
|
||||
print(json.dumps(arr))
|
||||
sys.exit(0)
|
||||
except: pass
|
||||
# Nothing found
|
||||
sys.exit(1)
|
||||
" <<< "$output" 2>/dev/null)
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$json_output" | jq . &>/dev/null; then
|
||||
error "Invalid JSON for $category_key. Raw output saved to $LOG_DIR/expand-${category_key}.raw"
|
||||
echo "$output" > "$LOG_DIR/expand-${category_key}.raw"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local count
|
||||
count=$(echo "$json_output" | jq 'length')
|
||||
success "$name: $count skills generated"
|
||||
|
||||
# Return the JSON
|
||||
echo "$json_output"
|
||||
}
|
||||
|
||||
# Build or update manifest
|
||||
build_manifest() {
|
||||
local categories_to_expand=("$@")
|
||||
|
||||
# Initialize manifest if it doesn't exist
|
||||
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
||||
echo '{"version":"1.0","created":"'"$(date +%Y-%m-%d)"'","categories":{}}' | jq . > "$MANIFEST_FILE"
|
||||
fi
|
||||
|
||||
local total=0
|
||||
local failed=0
|
||||
|
||||
for category_key in "${categories_to_expand[@]}"; do
|
||||
local skills_json
|
||||
if skills_json=$(expand_category "$category_key"); then
|
||||
# Add to manifest
|
||||
local dir
|
||||
dir=$(jq -r ".categories[\"$category_key\"].dir" "$CATEGORIES_FILE")
|
||||
local name
|
||||
name=$(jq -r ".categories[\"$category_key\"].name" "$CATEGORIES_FILE")
|
||||
local priority
|
||||
priority=$(jq -r ".categories[\"$category_key\"].priority" "$CATEGORIES_FILE")
|
||||
|
||||
local category_obj
|
||||
category_obj=$(jq -n \
|
||||
--arg name "$name" \
|
||||
--arg dir "$dir" \
|
||||
--arg priority "$priority" \
|
||||
--argjson skills "$skills_json" \
|
||||
'{name: $name, dir: $dir, priority: $priority, skills: $skills}')
|
||||
|
||||
# Merge into manifest
|
||||
jq --arg key "$category_key" --argjson cat "$category_obj" \
|
||||
'.categories[$key] = $cat' "$MANIFEST_FILE" > "$MANIFEST_FILE.tmp" \
|
||||
&& mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
|
||||
|
||||
local count
|
||||
count=$(echo "$skills_json" | jq 'length')
|
||||
total=$((total + count))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
# Rate limiting: pause between API calls
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "" >&2
|
||||
log "═══════════════════════════════════════"
|
||||
success "Total skills in manifest: $total"
|
||||
[[ $failed -gt 0 ]] && error "Failed categories: $failed"
|
||||
log "Manifest: $MANIFEST_FILE"
|
||||
log "═══════════════════════════════════════"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
parse_args() {
|
||||
local wave=""
|
||||
local single_category=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--wave)
|
||||
wave="$2"
|
||||
shift 2
|
||||
;;
|
||||
--model)
|
||||
MODEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [category-key] [--wave N] [--model MODEL]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " category-key Expand single category"
|
||||
echo " --wave N Expand all categories in wave N (1 or 2)"
|
||||
echo " --model MODEL Claude model to use (default: haiku)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Expand all categories"
|
||||
echo " $0 azure-ai-services # Expand single category"
|
||||
echo " $0 --wave 1 # Expand HIGH priority only"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
single_category="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Determine which categories to expand
|
||||
if [[ -n "$single_category" ]]; then
|
||||
echo "$single_category"
|
||||
elif [[ -n "$wave" ]]; then
|
||||
jq -r ".waves[] | select(.wave == $wave) | .categories[]" "$CATEGORIES_FILE"
|
||||
else
|
||||
jq -r '.categories | keys[]' "$CATEGORIES_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
check_prereqs
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log "Skill Category Expansion"
|
||||
log "Model: $MODEL | Categories file: $CATEGORIES_FILE"
|
||||
echo "" >&2
|
||||
|
||||
local categories=()
|
||||
while IFS= read -r line; do
|
||||
categories+=("$line")
|
||||
done < <(parse_args "$@")
|
||||
|
||||
if [[ ${#categories[@]} -eq 0 ]]; then
|
||||
error "No categories to expand"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Categories to expand: ${#categories[@]}"
|
||||
for cat in "${categories[@]}"; do
|
||||
log " - $cat"
|
||||
done
|
||||
echo "" >&2
|
||||
|
||||
build_manifest "${categories[@]}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
610
plugins/ms-ai-architect/scripts/skill-gen/generate-skills.sh
Executable file
610
plugins/ms-ai-architect/scripts/skill-gen/generate-skills.sh
Executable file
|
|
@ -0,0 +1,610 @@
|
|||
#!/bin/bash
|
||||
# generate-skills.sh — Generate knowledge reference files from manifest
|
||||
#
|
||||
# Reads manifest.json and generates each skill file using claude --print
|
||||
# with the prompt template. Supports resuming from where it left off.
|
||||
#
|
||||
# Usage:
|
||||
# ./generate-skills.sh # Generate all pending skills
|
||||
# ./generate-skills.sh --category rag-architecture # Generate single category
|
||||
# ./generate-skills.sh --skill azure-ai-vision-overview # Generate single skill
|
||||
# ./generate-skills.sh --wave 1 # Generate wave 1 (HIGH) only
|
||||
# ./generate-skills.sh --dry-run # Show what would be generated
|
||||
# ./generate-skills.sh --pilot 5 # Generate first N skills only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - claude CLI installed and authenticated
|
||||
# - jq installed
|
||||
# - manifest.json (run expand-categories.sh first)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
REFS_DIR="$PLUGIN_DIR/skills/ms-ai-engineering/references"
|
||||
MANIFEST_FILE="$SCRIPT_DIR/manifest.json"
|
||||
STATE_FILE="$SCRIPT_DIR/state.json"
|
||||
PROMPT_TEMPLATE="$SCRIPT_DIR/prompt-template.md"
|
||||
CATEGORIES_FILE="$SCRIPT_DIR/categories.json"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
|
||||
# Model for generation (sonnet for quality, haiku for speed)
|
||||
MODEL="${MODEL:-sonnet}"
|
||||
|
||||
# Limits
|
||||
PARALLEL="${PARALLEL:-1}" # Sequential by default for reliability
|
||||
DELAY="${DELAY:-3}" # Seconds between API calls
|
||||
MIN_SIZE="${MIN_SIZE:-5000}" # Minimum file size in bytes (quality gate)
|
||||
MAX_RETRIES="${MAX_RETRIES:-2}" # Retries for failed/small files
|
||||
|
||||
# Flags
|
||||
DRY_RUN=false
|
||||
PILOT=0
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[gen]${NC} $1" >&2; }
|
||||
success() { echo -e "${GREEN}[gen]${NC} $1" >&2; }
|
||||
warn() { echo -e "${YELLOW}[gen]${NC} $1" >&2; }
|
||||
error() { echo -e "${RED}[gen]${NC} $1" >&2; }
|
||||
detail() { echo -e "${CYAN}[gen]${NC} $1" >&2; }
|
||||
|
||||
# Check prerequisites
|
||||
check_prereqs() {
|
||||
if ! command -v claude &>/dev/null; then
|
||||
error "claude CLI not found"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq &>/dev/null; then
|
||||
error "jq not found"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
||||
error "manifest.json not found. Run expand-categories.sh first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize or load state
|
||||
init_state() {
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
jq -n '{
|
||||
"started": "'$(date -Iseconds)'",
|
||||
"completed": [],
|
||||
"failed": [],
|
||||
"skipped": [],
|
||||
"stats": {
|
||||
"total_generated": 0,
|
||||
"total_failed": 0,
|
||||
"total_skipped": 0,
|
||||
"total_bytes": 0
|
||||
}
|
||||
}' > "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if skill is already completed
|
||||
is_completed() {
|
||||
local skill_id="$1"
|
||||
jq -e --arg id "$skill_id" '.completed | index($id) != null' "$STATE_FILE" &>/dev/null
|
||||
}
|
||||
|
||||
# Mark skill as completed
|
||||
mark_completed() {
|
||||
local skill_id="$1"
|
||||
local file_size="$2"
|
||||
jq --arg id "$skill_id" --arg size "$file_size" '
|
||||
.completed += [$id] |
|
||||
.stats.total_generated += 1 |
|
||||
.stats.total_bytes += ($size | tonumber)
|
||||
' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Mark skill as failed
|
||||
mark_failed() {
|
||||
local skill_id="$1"
|
||||
local reason="$2"
|
||||
jq --arg id "$skill_id" --arg reason "$reason" '
|
||||
.failed += [{"id": $id, "reason": $reason, "time": (now | todate)}] |
|
||||
.stats.total_failed += 1
|
||||
' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Get existing context for a category (overlap files content summary)
|
||||
get_existing_context() {
|
||||
local category_key="$1"
|
||||
local overlaps
|
||||
overlaps=$(jq -r ".categories[\"$category_key\"].existing_overlap // [] | .[]" "$CATEGORIES_FILE" 2>/dev/null)
|
||||
|
||||
if [[ -z "$overlaps" ]]; then
|
||||
echo "Ingen direkte overlapp med eksisterende filer."
|
||||
return
|
||||
fi
|
||||
|
||||
local context=""
|
||||
for overlap in $overlaps; do
|
||||
local filepath="$REFS_DIR/$overlap"
|
||||
if [[ -f "$filepath" ]]; then
|
||||
# Extract just the header and section titles
|
||||
local summary
|
||||
summary=$(head -50 "$filepath" | grep -E '^#{1,3} ' | head -10)
|
||||
context+="**$overlap:** $summary"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${context:-Ingen direkte overlapp med eksisterende filer.}"
|
||||
}
|
||||
|
||||
# Get related skills in same category
|
||||
get_related_skills() {
|
||||
local category_key="$1"
|
||||
local current_skill="$2"
|
||||
|
||||
jq -r --arg key "$category_key" --arg current "$current_skill" '
|
||||
.categories[$key].skills[]
|
||||
| select(.id != $current)
|
||||
| "- \(.title): \(.description)"
|
||||
' "$MANIFEST_FILE" | head -20
|
||||
}
|
||||
|
||||
# Build the prompt for a specific skill
|
||||
build_prompt() {
|
||||
local category_key="$1"
|
||||
local skill_id="$2"
|
||||
|
||||
local title description subtopics
|
||||
title=$(jq -r --arg key "$category_key" --arg id "$skill_id" \
|
||||
'.categories[$key].skills[] | select(.id == $id) | .title' "$MANIFEST_FILE")
|
||||
description=$(jq -r --arg key "$category_key" --arg id "$skill_id" \
|
||||
'.categories[$key].skills[] | select(.id == $id) | .description' "$MANIFEST_FILE")
|
||||
subtopics=$(jq -r --arg key "$category_key" --arg id "$skill_id" \
|
||||
'.categories[$key].skills[] | select(.id == $id) | .subtopics | join(", ")' "$MANIFEST_FILE")
|
||||
|
||||
local category_name category_description
|
||||
category_name=$(jq -r --arg key "$category_key" '.categories[$key].name' "$MANIFEST_FILE")
|
||||
category_description=$(jq -r --arg key "$category_key" '.categories[$key].name' "$CATEGORIES_FILE")
|
||||
|
||||
local existing_context
|
||||
existing_context=$(get_existing_context "$category_key")
|
||||
|
||||
local related_skills
|
||||
related_skills=$(get_related_skills "$category_key" "$skill_id")
|
||||
|
||||
# Build the full prompt from template
|
||||
cat <<PROMPT
|
||||
Du er Cosmo Skyberg, en senior Microsoft AI Solution Architect som skriver kunnskapsreferanser for et Claude Code-plugin. Referansene brukes av en AI-arkitekt persona som hjelper norske organisasjoner (spesielt offentlig sektor) med Microsoft AI-løsninger.
|
||||
|
||||
## Oppgave
|
||||
|
||||
Skriv en komplett kunnskapsreferanse om: **${title}**
|
||||
|
||||
Skill-beskrivelse: ${description}
|
||||
Viktige undertemaer å dekke: ${subtopics}
|
||||
|
||||
Denne filen tilhører kategorien **${category_name}**.
|
||||
|
||||
## Format-krav (STRENGT)
|
||||
|
||||
### Header
|
||||
\`\`\`markdown
|
||||
# ${title}
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** [GA | Preview | Announced]
|
||||
**Category:** ${category_name}
|
||||
|
||||
---
|
||||
\`\`\`
|
||||
|
||||
### Innhold (7-15 KB, alle seksjoner påkrevd)
|
||||
|
||||
1. **Introduksjon** (2-3 avsnitt)
|
||||
- Hva er dette? Hvorfor er det viktig for enterprise AI?
|
||||
- Plassering i Microsoft-økosystemet
|
||||
- Norsk prosa, engelske tekniske termer
|
||||
|
||||
2. **Kjernekomponenter / Nøkkelegenskaper**
|
||||
- Bruk tabeller for sammenligninger
|
||||
- Bullet points for egenskaper
|
||||
- Kodeeksempler der relevant (korte, illustrative)
|
||||
|
||||
3. **Arkitekturmønstre**
|
||||
- 2-3 typiske bruksmønstre
|
||||
- Når bruke hvert mønster
|
||||
- Fordeler og ulemper
|
||||
|
||||
4. **Beslutningsveiledning**
|
||||
- "Velg X når..." beslutningstabell
|
||||
- Vanlige feil og misforståelser
|
||||
- Røde flagg arkitekten bør se etter
|
||||
|
||||
5. **Integrasjon med Microsoft-stakken**
|
||||
- Hvordan dette kobles til andre Azure/M365-tjenester
|
||||
- Typiske integrasjonsmønstre
|
||||
|
||||
6. **Offentlig sektor (Norge)**
|
||||
- GDPR, Schrems II, AI Act, Forvaltningsloven
|
||||
- Datasuverenitet og residency
|
||||
|
||||
7. **Kostnad og lisensiering**
|
||||
- Prismodell (oversikt)
|
||||
- Kostnadsoptimaliseringstips
|
||||
|
||||
8. **For arkitekten (Cosmo)**
|
||||
- 5-8 nøkkelspørsmål å stille kunden
|
||||
- Vanlige fallgruver
|
||||
- Anbefalinger per modenhetsnivå
|
||||
|
||||
9. **Kilder og verifisering**
|
||||
- Microsoft Learn-referanser
|
||||
- Konfidensnivå (Verified / Baseline / Assumed)
|
||||
|
||||
## Regler
|
||||
|
||||
1. Norsk prosa, engelske tekniske termer
|
||||
2. Tabeller over tekst for sammenligninger
|
||||
3. Konkret over vagt — spesifikke tall og tjenester
|
||||
4. Balansert — vis fordeler OG ulemper
|
||||
5. Oppdatert — 2025-2026 informasjon
|
||||
6. Størrelse: 7-15 KB (200-400 linjer)
|
||||
7. Ikke dupliser: ${existing_context}
|
||||
|
||||
## Relaterte skills (for kryssreferanser)
|
||||
${related_skills}
|
||||
|
||||
Skriv KUN markdown-innholdet. Ingen innledende forklaring eller avsluttende kommentar.
|
||||
PROMPT
|
||||
}
|
||||
|
||||
# Generate a single skill file
|
||||
generate_skill() {
|
||||
local category_key="$1"
|
||||
local skill_id="$2"
|
||||
local attempt="${3:-1}"
|
||||
|
||||
local category_dir
|
||||
category_dir=$(jq -r --arg key "$category_key" '.categories[$key].dir' "$MANIFEST_FILE")
|
||||
|
||||
local output_dir="$REFS_DIR/$category_dir"
|
||||
local output_file="$output_dir/$skill_id.md"
|
||||
|
||||
local title
|
||||
title=$(jq -r --arg key "$category_key" --arg id "$skill_id" \
|
||||
'.categories[$key].skills[] | select(.id == $id) | .title' "$MANIFEST_FILE")
|
||||
|
||||
# Skip if already completed
|
||||
if is_completed "$skill_id"; then
|
||||
detail " Skipping (already completed): $skill_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Skip if file already exists and is large enough
|
||||
if [[ -f "$output_file" ]]; then
|
||||
local existing_size
|
||||
existing_size=$(wc -c < "$output_file" | tr -d ' ')
|
||||
if [[ $existing_size -ge $MIN_SIZE ]]; then
|
||||
detail " Skipping (file exists, ${existing_size}B): $skill_id"
|
||||
mark_completed "$skill_id" "$existing_size"
|
||||
return 0
|
||||
fi
|
||||
warn " File exists but too small (${existing_size}B < ${MIN_SIZE}B), regenerating: $skill_id"
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
log " [DRY RUN] Would generate: $output_file"
|
||||
log " Title: $title"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log " Generating ($attempt/$((MAX_RETRIES+1))): $title"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Build prompt
|
||||
local prompt
|
||||
prompt=$(build_prompt "$category_key" "$skill_id")
|
||||
|
||||
# Generate with claude --print
|
||||
local output
|
||||
if ! output=$(claude --print --model "$MODEL" "$prompt" 2>"$LOG_DIR/gen-${skill_id}.err"); then
|
||||
error " Claude CLI failed for $skill_id"
|
||||
if [[ $attempt -le $MAX_RETRIES ]]; then
|
||||
warn " Retrying in ${DELAY}s..."
|
||||
sleep "$DELAY"
|
||||
generate_skill "$category_key" "$skill_id" $((attempt + 1))
|
||||
return $?
|
||||
fi
|
||||
mark_failed "$skill_id" "claude CLI error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Write output
|
||||
echo "$output" > "$output_file"
|
||||
|
||||
# Quality gate: check file size
|
||||
local file_size
|
||||
file_size=$(wc -c < "$output_file" | tr -d ' ')
|
||||
|
||||
if [[ $file_size -lt $MIN_SIZE ]]; then
|
||||
warn " File too small: ${file_size}B (min: ${MIN_SIZE}B)"
|
||||
if [[ $attempt -le $MAX_RETRIES ]]; then
|
||||
warn " Retrying with stronger prompt..."
|
||||
sleep "$DELAY"
|
||||
generate_skill "$category_key" "$skill_id" $((attempt + 1))
|
||||
return $?
|
||||
fi
|
||||
error " Giving up on $skill_id (still too small after retries)"
|
||||
mark_failed "$skill_id" "file too small: ${file_size}B"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Quality gate: check that file starts with # (markdown header)
|
||||
if ! head -1 "$output_file" | grep -q '^# '; then
|
||||
warn " File doesn't start with markdown header"
|
||||
# Try to fix by removing leading content before first header
|
||||
local temp_file="$output_file.tmp"
|
||||
sed -n '/^# /,$p' "$output_file" > "$temp_file"
|
||||
if [[ -s "$temp_file" ]]; then
|
||||
mv "$temp_file" "$output_file"
|
||||
file_size=$(wc -c < "$output_file" | tr -d ' ')
|
||||
else
|
||||
rm -f "$temp_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
mark_completed "$skill_id" "$file_size"
|
||||
success " Generated: $skill_id (${file_size}B)"
|
||||
|
||||
# Rate limiting
|
||||
sleep "$DELAY"
|
||||
}
|
||||
|
||||
# Generate all skills in a category
|
||||
generate_category() {
|
||||
local category_key="$1"
|
||||
|
||||
local category_name
|
||||
category_name=$(jq -r --arg key "$category_key" '.categories[$key].name' "$MANIFEST_FILE")
|
||||
local skill_count
|
||||
skill_count=$(jq --arg key "$category_key" '.categories[$key].skills | length' "$MANIFEST_FILE")
|
||||
|
||||
log ""
|
||||
log "═══════════════════════════════════════"
|
||||
log "Category: $category_name ($skill_count skills)"
|
||||
log "═══════════════════════════════════════"
|
||||
|
||||
local skill_ids=()
|
||||
while IFS= read -r line; do
|
||||
skill_ids+=("$line")
|
||||
done < <(jq -r --arg key "$category_key" \
|
||||
'.categories[$key].skills[].id' "$MANIFEST_FILE")
|
||||
|
||||
local generated=0
|
||||
for skill_id in "${skill_ids[@]}"; do
|
||||
if generate_skill "$category_key" "$skill_id"; then
|
||||
generated=$((generated + 1))
|
||||
fi
|
||||
|
||||
# Pilot mode: stop after N skills total
|
||||
if [[ $PILOT -gt 0 ]]; then
|
||||
local total_completed
|
||||
total_completed=$(jq '.stats.total_generated' "$STATE_FILE")
|
||||
if [[ $total_completed -ge $PILOT ]]; then
|
||||
warn "Pilot limit reached ($PILOT skills)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
success "Category complete: $generated/$skill_count generated"
|
||||
}
|
||||
|
||||
# Print summary
|
||||
print_summary() {
|
||||
local total_generated total_failed total_bytes
|
||||
total_generated=$(jq '.stats.total_generated' "$STATE_FILE")
|
||||
total_failed=$(jq '.stats.total_failed' "$STATE_FILE")
|
||||
total_bytes=$(jq '.stats.total_bytes' "$STATE_FILE")
|
||||
|
||||
local total_kb=$((total_bytes / 1024))
|
||||
|
||||
echo ""
|
||||
log "═══════════════════════════════════════"
|
||||
log " GENERATION SUMMARY "
|
||||
log "═══════════════════════════════════════"
|
||||
success "Generated: $total_generated files ($total_kb KB)"
|
||||
[[ $total_failed -gt 0 ]] && error "Failed: $total_failed files"
|
||||
log "State: $STATE_FILE"
|
||||
log "Output: $REFS_DIR/"
|
||||
log "═══════════════════════════════════════"
|
||||
|
||||
# List failed skills if any
|
||||
if [[ $total_failed -gt 0 ]]; then
|
||||
echo ""
|
||||
warn "Failed skills:"
|
||||
jq -r '.failed[] | " - \(.id): \(.reason)"' "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
parse_args() {
|
||||
local category=""
|
||||
local skill=""
|
||||
local wave=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--category|-c)
|
||||
category="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skill|-s)
|
||||
skill="$2"
|
||||
shift 2
|
||||
;;
|
||||
--wave|-w)
|
||||
wave="$2"
|
||||
shift 2
|
||||
;;
|
||||
--model|-m)
|
||||
MODEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--pilot)
|
||||
PILOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--delay)
|
||||
DELAY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--min-size)
|
||||
MIN_SIZE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-retries)
|
||||
MAX_RETRIES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--reset)
|
||||
rm -f "$STATE_FILE"
|
||||
log "State reset"
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--category, -c KEY Generate single category
|
||||
--skill, -s ID Generate single skill
|
||||
--wave, -w N Generate wave N (1=HIGH, 2=MEDIUM)
|
||||
--model, -m MODEL Claude model (default: sonnet)
|
||||
--dry-run Show what would be generated
|
||||
--pilot N Generate only first N skills (for testing)
|
||||
--delay N Seconds between API calls (default: 3)
|
||||
--min-size N Minimum file size in bytes (default: 5000)
|
||||
--max-retries N Max retries per skill (default: 2)
|
||||
--reset Clear state and start fresh
|
||||
|
||||
Environment:
|
||||
MODEL=sonnet Override default model
|
||||
MAX_BUDGET_USD=5 Max dollar amount per run
|
||||
PARALLEL=1 Parallel generation (experimental)
|
||||
DELAY=3 Delay between calls
|
||||
|
||||
Examples:
|
||||
$0 --pilot 3 # Test with 3 skills
|
||||
$0 --category rag-architecture # Generate one category
|
||||
$0 --wave 1 --model sonnet # Generate all HIGH priority
|
||||
$0 --dry-run # Preview without generating
|
||||
MODEL=haiku $0 --wave 2 # MEDIUM priority with haiku
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Return mode and target
|
||||
if [[ -n "$skill" ]]; then
|
||||
echo "skill:$skill"
|
||||
elif [[ -n "$category" ]]; then
|
||||
echo "category:$category"
|
||||
elif [[ -n "$wave" ]]; then
|
||||
echo "wave:$wave"
|
||||
else
|
||||
echo "all"
|
||||
fi
|
||||
}
|
||||
|
||||
# Find which category a skill belongs to
|
||||
find_skill_category() {
|
||||
local skill_id="$1"
|
||||
jq -r --arg id "$skill_id" '
|
||||
.categories | to_entries[] |
|
||||
select(.value.skills | map(.id) | index($id) != null) |
|
||||
.key
|
||||
' "$MANIFEST_FILE"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
check_prereqs
|
||||
init_state
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
local mode
|
||||
mode=$(parse_args "$@")
|
||||
|
||||
log "Skill Generation Pipeline"
|
||||
log "Model: $MODEL | Min size: ${MIN_SIZE}B | Delay: ${DELAY}s"
|
||||
$DRY_RUN && warn "DRY RUN MODE — no files will be generated"
|
||||
[[ $PILOT -gt 0 ]] && warn "PILOT MODE — only $PILOT skills"
|
||||
echo ""
|
||||
|
||||
case "$mode" in
|
||||
skill:*)
|
||||
local skill_id="${mode#skill:}"
|
||||
local category_key
|
||||
category_key=$(find_skill_category "$skill_id")
|
||||
if [[ -z "$category_key" ]]; then
|
||||
error "Skill not found in manifest: $skill_id"
|
||||
exit 1
|
||||
fi
|
||||
generate_skill "$category_key" "$skill_id"
|
||||
;;
|
||||
category:*)
|
||||
local category_key="${mode#category:}"
|
||||
generate_category "$category_key"
|
||||
;;
|
||||
wave:*)
|
||||
local wave_num="${mode#wave:}"
|
||||
local categories=()
|
||||
while IFS= read -r line; do
|
||||
categories+=("$line")
|
||||
done < <(jq -r --argjson w "$wave_num" \
|
||||
'.waves[] | select(.wave == $w) | .categories[]' "$CATEGORIES_FILE")
|
||||
for cat in "${categories[@]}"; do
|
||||
generate_category "$cat"
|
||||
if [[ $PILOT -gt 0 ]]; then
|
||||
local total
|
||||
total=$(jq '.stats.total_generated' "$STATE_FILE")
|
||||
[[ $total -ge $PILOT ]] && break
|
||||
fi
|
||||
done
|
||||
;;
|
||||
all)
|
||||
local all_categories=()
|
||||
while IFS= read -r line; do
|
||||
all_categories+=("$line")
|
||||
done < <(jq -r '.categories | keys[]' "$MANIFEST_FILE")
|
||||
for cat in "${all_categories[@]}"; do
|
||||
generate_category "$cat"
|
||||
if [[ $PILOT -gt 0 ]]; then
|
||||
local total
|
||||
total=$(jq '.stats.total_generated' "$STATE_FILE")
|
||||
[[ $total -ge $PILOT ]] && break
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
4001
plugins/ms-ai-architect/scripts/skill-gen/manifest.json
Normal file
4001
plugins/ms-ai-architect/scripts/skill-gen/manifest.json
Normal file
File diff suppressed because it is too large
Load diff
102
plugins/ms-ai-architect/scripts/skill-gen/prompt-template.md
Normal file
102
plugins/ms-ai-architect/scripts/skill-gen/prompt-template.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Skill Generation Prompt Template
|
||||
|
||||
This template is used by `generate-skills.sh` to produce knowledge reference files for the architect plugin.
|
||||
|
||||
## Variables
|
||||
|
||||
- `{{SKILL_TITLE}}` — English title (e.g., "Azure AI Vision - Overview and Architecture")
|
||||
- `{{SKILL_FILENAME}}` — Kebab-case filename without extension (e.g., "azure-ai-vision-overview")
|
||||
- `{{CATEGORY_NAME}}` — Category name (e.g., "Azure AI Services (Foundry Tools)")
|
||||
- `{{CATEGORY_DIR}}` — Directory name (e.g., "azure-ai-services")
|
||||
- `{{CATEGORY_DESCRIPTION}}` — Category context
|
||||
- `{{RELATED_SKILLS}}` — Other skills in same category (for cross-referencing)
|
||||
- `{{EXISTING_CONTEXT}}` — Summary of existing reference files that overlap
|
||||
|
||||
## Prompt
|
||||
|
||||
```
|
||||
Du er Cosmo Skyberg, en senior Microsoft AI Solution Architect som skriver kunnskapsreferanser for et Claude Code-plugin. Referansene brukes av en AI-arkitekt persona som hjelper norske organisasjoner (spesielt offentlig sektor) med Microsoft AI-løsninger.
|
||||
|
||||
## Oppgave
|
||||
|
||||
Skriv en komplett kunnskapsreferanse om: **{{SKILL_TITLE}}**
|
||||
|
||||
Denne filen tilhører kategorien **{{CATEGORY_NAME}}**: {{CATEGORY_DESCRIPTION}}
|
||||
|
||||
## Format-krav (STRENGT)
|
||||
|
||||
Filen MÅ følge dette eksakte formatet:
|
||||
|
||||
### Header
|
||||
```markdown
|
||||
# {{SKILL_TITLE}}
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** [GA | Preview | Announced]
|
||||
**Category:** {{CATEGORY_NAME}}
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### Innhold (7-15 KB, alle seksjoner påkrevd)
|
||||
|
||||
1. **Introduksjon** (2-3 avsnitt)
|
||||
- Hva er dette? Hvorfor er det viktig for enterprise AI?
|
||||
- Plassering i Microsoft-økosystemet
|
||||
- Norsk prosa, engelske tekniske termer
|
||||
|
||||
2. **Kjernekomponenter / Nøkkelegenskaper**
|
||||
- Bruk tabeller for sammenligninger
|
||||
- Bullet points for egenskaper
|
||||
- Kodeeksempler der relevant (korte, illustrative)
|
||||
|
||||
3. **Arkitekturmønstre**
|
||||
- 2-3 typiske bruksmønstre med ASCII-diagrammer der det hjelper
|
||||
- Når bruke hvert mønster
|
||||
- Fordeler og ulemper
|
||||
|
||||
4. **Beslutningsveiledning**
|
||||
- "Velg X når..." beslutningstabell
|
||||
- Vanlige feil og misforståelser
|
||||
- Røde flagg arkitekten bør se etter
|
||||
|
||||
5. **Integrasjon med Microsoft-stakken**
|
||||
- Hvordan dette kobles til andre Azure/M365-tjenester
|
||||
- Typiske integrasjonsmønstre
|
||||
|
||||
6. **Offentlig sektor (Norge)**
|
||||
- Spesielle hensyn for norsk offentlig sektor
|
||||
- GDPR, Schrems II, AI Act, Forvaltningsloven
|
||||
- Datasuverenitet og residency
|
||||
|
||||
7. **Kostnad og lisensiering**
|
||||
- Prismodell (oversikt, ikke detaljerte tall)
|
||||
- Inkludert i hvilke lisenser
|
||||
- Kostnadsoptimaliseringstips
|
||||
|
||||
8. **For arkitekten (Cosmo)**
|
||||
- 5-8 nøkkelspørsmål å stille kunden
|
||||
- Vanlige fallgruver
|
||||
- Anbefalinger per modenhetsnivå (starter/intermediate/advanced)
|
||||
|
||||
9. **Kilder og verifisering**
|
||||
- Referanser til Microsoft Learn-artikler
|
||||
- Sist verifisert dato
|
||||
- Konfidensnivå per seksjon (Verified / Baseline / Assumed)
|
||||
|
||||
## Regler
|
||||
|
||||
1. **Norsk prosa, engelske tekniske termer** — Skriv forklaringer på norsk, behold engelske termer for tjenester, konsepter og API-er
|
||||
2. **Tabeller over tekst** — Bruk tabeller for sammenligninger, beslutninger, oversikter
|
||||
3. **Konkret over vagt** — Spesifikke tall, konkrete eksempler, navngitte tjenester
|
||||
4. **Balansert** — Vis både fordeler og ulemper, ikke bare markedsføringssnakk
|
||||
5. **Oppdatert** — Bruk 2025-2026 informasjon, nevn GPT-5, AI Act, Foundry Tools (ny branding)
|
||||
6. **Confidence markers** — Merk usikre påstander med (anslått) eller (uverifisert)
|
||||
7. **Størrelse** — Mål: 7-15 KB (200-400 linjer). Ikke for kort (overfladisk) eller for langt (bloat)
|
||||
8. **Ingen duplikering** — Ikke gjenta informasjon som allerede finnes i: {{EXISTING_CONTEXT}}
|
||||
|
||||
## Relaterte skills i samme kategori
|
||||
{{RELATED_SKILLS}}
|
||||
|
||||
Skriv KUN markdown-innholdet. Ingen innledende forklaring eller avsluttende kommentar.
|
||||
```
|
||||
227
plugins/ms-ai-architect/scripts/skill-gen/state.json
Normal file
227
plugins/ms-ai-architect/scripts/skill-gen/state.json
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
{
|
||||
"started": "2026-02-03T15:13:03+01:00",
|
||||
"completed": [
|
||||
"rag-core-patterns",
|
||||
"azure-ai-search-setup",
|
||||
"embedding-models-selection",
|
||||
"vector-indexing-techniques",
|
||||
"chunking-strategies",
|
||||
"hybrid-search-configuration",
|
||||
"semantic-ranker-reranking",
|
||||
"citation-tracking",
|
||||
"rag-evaluation-frameworks",
|
||||
"multi-index-federation",
|
||||
"rag-security-rbac",
|
||||
"rag-caching-optimization",
|
||||
"metadata-management-filtering",
|
||||
"graphrag-knowledge-graphs",
|
||||
"rag-query-understanding",
|
||||
"rag-context-windows",
|
||||
"streaming-rag-responses",
|
||||
"rag-iterative-refinement",
|
||||
"rag-enterprise-scale",
|
||||
"rag-document-preprocessing",
|
||||
"rag-hallucination-mitigation",
|
||||
"rag-cost-optimization",
|
||||
"azure-ai-vision-ocr-processing",
|
||||
"azure-ai-vision-image-analysis",
|
||||
"document-intelligence-prebuilt-models",
|
||||
"document-intelligence-custom-models",
|
||||
"speech-services-speech-to-text",
|
||||
"speech-services-text-to-speech",
|
||||
"speech-services-speaker-recognition",
|
||||
"language-services-text-analytics",
|
||||
"language-services-question-answering",
|
||||
"language-services-custom-text-classification",
|
||||
"translator-document-translation",
|
||||
"translator-custom-neural-models",
|
||||
"content-understanding-multimodal-analysis",
|
||||
"ai-services-networking-security",
|
||||
"ai-services-monitoring-logging",
|
||||
"ai-services-api-best-practices",
|
||||
"ai-services-governance-compliance",
|
||||
"ai-services-cost-optimization",
|
||||
"ai-services-enterprise-architecture",
|
||||
"ai-services-vs-foundry-tools-selection",
|
||||
"responsible-ai-framework-overview",
|
||||
"ai-act-compliance-guide",
|
||||
"bias-detection-mitigation-strategies",
|
||||
"ai-governance-structure-framework",
|
||||
"ai-center-of-excellence-setup",
|
||||
"red-teaming-ai-models",
|
||||
"model-explainability-interpretability",
|
||||
"content-safety-implementation",
|
||||
"ai-impact-assessment-framework",
|
||||
"transparency-documentation-standards",
|
||||
"gdpr-compliance-ai-systems",
|
||||
"algorithmic-accountability-auditability",
|
||||
"fairness-testing-measurement",
|
||||
"ai-ethics-in-public-sector",
|
||||
"model-monitoring-drift-detection",
|
||||
"stakeholder-communication-ai-decisions",
|
||||
"ai-risk-taxonomy-classification",
|
||||
"responsible-ai-policy-development",
|
||||
"data-quality-responsible-ai",
|
||||
"human-in-the-loop-oversight",
|
||||
"responsible-ai-training-awareness",
|
||||
"continuous-improvement-feedback-loops",
|
||||
"declarative-agents-fundamentals",
|
||||
"custom-engine-agents-development",
|
||||
"copilot-studio-topics-and-entities",
|
||||
"microsoft-graph-api-copilot-integration",
|
||||
"copilot-connectors-design-patterns",
|
||||
"mcp-protocol-copilot-studio",
|
||||
"teams-copilot-message-extensions",
|
||||
"sharepoint-copilot-agents",
|
||||
"m365-copilot-plugins-ecosystem",
|
||||
"copilot-orchestration-multi-agent",
|
||||
"copilot-dlp-and-governance",
|
||||
"copilot-analytics-and-usage-insights",
|
||||
"copilot-prompt-engineering-governance",
|
||||
"declarative-agents-grounding-strategies",
|
||||
"copilot-studio-nlp-configuration",
|
||||
"copilot-extensibility-security-patterns",
|
||||
"power-automate-copilot-integration",
|
||||
"copilot-context-window-optimization",
|
||||
"adaptive-cards-copilot-responses",
|
||||
"copilot-api-rate-limiting-resilience",
|
||||
"copilot-studio-localization-globalization",
|
||||
"enterprise-governance-copilot-deployment",
|
||||
"system-message-design-patterns",
|
||||
"few-shot-learning-techniques",
|
||||
"chain-of-thought-prompting",
|
||||
"reasoning-models-o1-o3-optimization",
|
||||
"structured-output-formatting",
|
||||
"function-calling-and-tool-use",
|
||||
"grounding-and-knowledge-injection",
|
||||
"temperature-sampling-and-parameters",
|
||||
"token-optimization-and-efficiency",
|
||||
"prompt-testing-and-evaluation",
|
||||
"adversarial-prompting-and-jailbreaks",
|
||||
"multi-turn-conversation-management",
|
||||
"role-playing-and-persona-techniques",
|
||||
"error-handling-and-fallback-prompting",
|
||||
"domain-specific-prompt-optimization",
|
||||
"multimodal-prompt-design",
|
||||
"real-time-reasoning-performance",
|
||||
"regulatory-and-compliance-prompting",
|
||||
"token-counting-optimization",
|
||||
"semantic-caching-patterns",
|
||||
"reserved-capacity-planning",
|
||||
"model-selection-price-performance",
|
||||
"ptu-vs-paygo-economics",
|
||||
"batch-processing-cost-reduction",
|
||||
"azure-cost-management-ai",
|
||||
"request-batching-aggregation",
|
||||
"prompt-engineering-cost-reduction",
|
||||
"vector-storage-cost-optimization",
|
||||
"ai-builder-credits-transition",
|
||||
"cost-allocation-chargeback",
|
||||
"budget-forecasting-ai-projects",
|
||||
"small-language-models-economics",
|
||||
"rag-query-cost-reduction",
|
||||
"azure-ai-foundry-cost-governance",
|
||||
"multi-model-strategy-costs",
|
||||
"inference-endpoint-cost-optimization",
|
||||
"licensing-compliance-cost-avoidance",
|
||||
"observability-cost-reduction",
|
||||
"mlops-fundamentals-overview",
|
||||
"azure-ml-pipelines-orchestration",
|
||||
"model-versioning-registry-management",
|
||||
"ci-cd-for-ml-models",
|
||||
"contextual-retrieval",
|
||||
"late-chunking-patterns",
|
||||
"hierarchical-rag-patterns",
|
||||
"agentic-rag-patterns",
|
||||
"self-reflective-rag",
|
||||
"multimodal-rag",
|
||||
"model-evaluation-frameworks",
|
||||
"ab-testing-llm-applications",
|
||||
"data-drift-monitoring-detection",
|
||||
"model-drift-performance-degradation",
|
||||
"automated-retraining-pipelines",
|
||||
"prompt-flow-production-deployment",
|
||||
"model-deployment-strategies-azure",
|
||||
"inferencing-optimization-caching",
|
||||
"llm-evaluation-production",
|
||||
"monitoring-observability-ml-systems",
|
||||
"governance-audit-ml-operations",
|
||||
"genaiops-llm-specific-practices",
|
||||
"cost-optimization-mlops-pipelines",
|
||||
"infrastructure-as-code-mlops",
|
||||
"mlops-security-access-control",
|
||||
"feedback-loops-continuous-improvement",
|
||||
"responsible-ai-mlops-integration",
|
||||
"mlops-teams-collaboration-tools",
|
||||
"utredningsinstruksen-ai-methodology",
|
||||
"forvaltningsloven-ai-decisions",
|
||||
"digdir-principle-1-user-centric-design",
|
||||
"digdir-principle-2-interoperability",
|
||||
"digdir-principle-4-trust-security",
|
||||
"digital-samhandling-eif-5-layers",
|
||||
"dpia-norwegian-methodology-ai",
|
||||
"ros-analyse-ai-systems",
|
||||
"nsm-grunnprinsipper-ai-mapping",
|
||||
"anskaffelser-ai-procurement-framework",
|
||||
"gevinstrealisering-ai-projects",
|
||||
"norge-ai-strategy-government",
|
||||
"digdir-ai-governance-structure",
|
||||
"statistical-ethics-ssa-methodology",
|
||||
"public-sector-ai-ethics-framework",
|
||||
"accessibility-requirements-wcag-norway",
|
||||
"copyright-ai-training-data-norway",
|
||||
"budget-and-accounting-ai-costs",
|
||||
"digital-accessibility-action-plan",
|
||||
"citizen-communication-ai-decisions",
|
||||
"prompt-injection-defense-patterns",
|
||||
"jailbreak-prevention-production",
|
||||
"content-safety-filter-calibration",
|
||||
"pii-detection-norwegian-context",
|
||||
"ai-threat-modeling-stride",
|
||||
"ai-security-scoring-framework",
|
||||
"ai-incident-response-procedures",
|
||||
"output-validation-grounding-verification",
|
||||
"zero-trust-ai-services",
|
||||
"data-leakage-prevention-ai",
|
||||
"adversarial-input-robustness-testing",
|
||||
"model-fingerprinting-watermarking",
|
||||
"secure-model-deployment-hardening",
|
||||
"ai-red-team-operations-practical",
|
||||
"supply-chain-security-ai-models",
|
||||
"azure-monitor-setup-ai-workloads",
|
||||
"application-insights-llm-monitoring",
|
||||
"token-usage-tracking-attribution",
|
||||
"anomaly-detection-ai-systems",
|
||||
"custom-dashboards-ai-operations",
|
||||
"log-analytics-kql-ai-queries",
|
||||
"distributed-tracing-ai-pipelines",
|
||||
"sla-monitoring-ai-services",
|
||||
"model-performance-drift-detection",
|
||||
"security-and-audit-logging-ai",
|
||||
"cost-monitoring-cost-attribution",
|
||||
"response-quality-metrics-rag",
|
||||
"endpoint-health-and-capacity-planning",
|
||||
"real-time-streaming-monitoring",
|
||||
"compliance-monitoring-ai-governance",
|
||||
"alerting-strategies-escalation",
|
||||
"observability-for-copilot-extensions",
|
||||
"data-residency-audit-monitoring",
|
||||
"multi-agent-orchestration-patterns",
|
||||
"agent-to-agent-communication",
|
||||
"semantic-kernel-agents-implementation",
|
||||
"agent-memory-and-context-management",
|
||||
"tool-use-and-function-calling-patterns",
|
||||
"agent-autonomy-and-control-governance",
|
||||
"agent-365-governance-and-deployment",
|
||||
"agent-evaluation-testing-frameworks",
|
||||
"autonomous-workflow-automation-patterns"
|
||||
],
|
||||
"failed": [],
|
||||
"skipped": [],
|
||||
"stats": {
|
||||
"total_generated": 214,
|
||||
"total_failed": 0,
|
||||
"total_skipped": 0,
|
||||
"total_bytes": 4673862
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue