Files
skillfactor-pipeline/adapters/claude/build_claude.py
skillfactor-pipeline 5c03e07ded fix(homepage): chat example in English, simpler case, real chat bubbles
Simpler story (slide decks unread -> three-bullet status email), proper
Claude-chat look with avatars and bubbles on both sides. Canonical trigger
prompt switched to English everywhere (homepage, architecture, both
adapter generators rebuilt) - V7 consistency green, all checks pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDKeXvpT6tENSvyQGLV1Uq
2026-07-10 06:01:43 +02:00

172 lines
6.9 KiB
Python

"""Claude adapter — builds an Agent-Skill package per profession from the
canonical content (knowledge/professions/<slug>/). Platform binding lives
HERE, never in the content files (ARCHITECTURE.md §2).
Output: adapters/claude/dist/<slug>/
SKILL.md trigger description + §3 contribution behaviour
competences/ copied canonical competence files (lazy references)
knowledge.md curated practitioner Q&A
vocabulary.json
plus adapters/claude/dist/marketplace.json (plugin-marketplace index).
Run: python adapters/claude/build_claude.py
Idempotent: dist/ is regenerated from scratch on every run.
"""
import json
import os
import re
import shutil
import sys
from datetime import date
A = os.path.dirname(os.path.abspath(__file__))
BASE = os.path.dirname(os.path.dirname(A))
K = os.path.join(BASE, "knowledge")
DIST = os.path.join(A, "dist")
sys.path.insert(0, os.path.join(K, "pipeline"))
from se_compile import PROFESSIONS # noqa: E402
# The canonical manual trigger — MUST stay identical to the homepage example
# (verified by qa/verify):
TRIGGER_PROMPT = ("Save the key insight from this chat as a lesson learned "
"in my SkillFactor repo.")
TRIGGER_VARIANTS = [
"What should we remember from this chat as a team? File it in the repo.",
"Log this as a lesson for my project <customer>.",
]
BEHAVIOUR = f"""## Contributing knowledge back (built-in behaviour)
You are not only preloaded with this profession — you are the interface
through which new experience flows back into the SkillFactor repos.
**When to offer a contribution (selective!):** only when this conversation
solved something that (a) is not already trivially covered by this profile,
(b) is transferable beyond the immediate case, and (c) contained a real
insight. Do not offer after routine answers.
**Manual trigger:** the user can always say:
> {TRIGGER_PROMPT}
(variants: "{TRIGGER_VARIANTS[0]}" · "{TRIGGER_VARIANTS[1]}")
**The switch — always propose up to two distillates:**
1. *Project distillate* (WITH customer/project reference) → the user's
private org repo, `projects/<customer>/<project>/lessons/`.
2. *Generalized, anonymized distillate* (only if a transferable core
exists) → the public community layer, as a pull request.
Ask: **"Both, just one, or neither?"**
**Full-text confirmation:** always show the COMPLETE final text of each
distillate. What gets committed is what the user confirmed — word for
word. Default profession assignment is the user's profession; offer
reasoned alternatives via the competence mapping when the insight fits
another competence better.
**Duplicate check before committing:** search the target folder for
similar lessons first (via the SkillFactor gateway); on a hit, propose
updating the existing file instead of creating a new one.
**Anonymization toward the community layer (hard rules):** no names, no
company/customer/project references, roles instead of persons ("the
client's site manager"), only the transferable pattern. Internally too:
roles instead of clear names where possible.
**Always a pull request, never a direct commit to main.** Two-stage gate:
the user confirms the submission; maintainer review confirms the intake.
Frontmatter follows `schemas/lesson.schema.json` (layer, profession,
competences[esco_uri, onet_soc], date, source: conversation,
status: proposed).
"""
def build_one(prof):
slug = prof["slug"]
src = os.path.join(K, "professions", slug)
if not os.path.isdir(src):
return None
out = os.path.join(DIST, slug)
os.makedirs(out, exist_ok=True)
comps = []
cdir = os.path.join(src, "competences")
if os.path.isdir(cdir):
os.makedirs(os.path.join(out, "competences"), exist_ok=True)
for f in sorted(os.listdir(cdir)):
if f.endswith(".md") and not f.startswith("_"):
shutil.copy(os.path.join(cdir, f),
os.path.join(out, "competences", f))
comps.append(f)
for f in ("knowledge.md", "vocabulary.json", "index.md"):
p = os.path.join(src, f)
if os.path.exists(p):
shutil.copy(p, os.path.join(out, f))
# trigger description from competence labels (first 12)
labels = []
for f in comps[:12]:
head = open(os.path.join(cdir, f), encoding="utf-8").read(400)
m = re.search(r'esco_label: "?([^"\n]+)"?', head)
if m:
labels.append(m.group(1))
trig = "; ".join(labels[:8]) or f"typical {prof['title']} work"
lines = [
"---",
f"name: skillfactor-{slug}",
"description: " + json.dumps(
f"Occupational skill layer for the profession '{prof['title']}'. "
f"Load when the user works as (or asks about the work of) a "
f"{prof['title']} — e.g. {trig}. Also handles saving lessons "
f"learned to the user's SkillFactor repos on request."),
"---", "",
f"# {prof['title']} — SkillFactor",
"",
f"Preloaded professional experience for {prof['title']}: curated "
"practitioner knowledge, competence-level explanations and the "
"vocabulary of the trade. Load references lazily:",
"",
"- [knowledge.md](knowledge.md) — curated practitioner Q&A "
"(CC-BY-SA attributed)",
f"- [competences/](competences/) — {len(comps)} competence files "
"(practice-focused explanation + grounded Q&A each)",
"- [vocabulary.json](vocabulary.json) — the trade's key terms",
"",
"Retrieval cascade when connected to the SkillFactor gateway: this "
"community profile → the user's org overlay → the active project's "
"lessons. More specific beats more general.",
"",
BEHAVIOUR,
"---",
f"*SkillFactor · generated {date.today().isoformat()} by "
"adapters/claude/build_claude.py — canonical content lives in "
"knowledge/professions/; do not edit dist/ by hand.*",
]
open(os.path.join(out, "SKILL.md"), "w", encoding="utf-8",
newline="\n").write("\n".join(lines))
return {"name": f"skillfactor-{slug}", "title": prof["title"],
"slug": slug, "competences": len(comps),
"source": f"knowledge/professions/{slug}"}
def main():
if os.path.isdir(DIST):
shutil.rmtree(DIST)
os.makedirs(DIST, exist_ok=True)
entries = [e for e in (build_one(p) for p in PROFESSIONS) if e]
market = {
"name": "skillfactor-professions",
"version": date.today().isoformat(),
"description": "SkillFactor occupational skill layer — profession "
"packages with built-in lesson-learned contribution.",
"plugins": entries,
}
json.dump(market, open(os.path.join(DIST, "marketplace.json"), "w",
encoding="utf-8"), indent=2)
print(f"built {len(entries)} skill packages -> {DIST}")
if __name__ == "__main__":
main()