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
172 lines
6.9 KiB
Python
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()
|