FL_50 Adapters — 배포 형태별 export 생성기 (Claude Code·Ollama·LM Studio·SillyTavern)
한 줄 요약
FableLayer Layer 5는 단일 정본 프롬프트(render_markdown(default_core())) 하나를 네 가지 배포 형태(Ollama Modelfile, LM Studio preset, SillyTavern preset, Claude Code plugin)로 변환하는 export 생성기다. 왜 배우나: 같은 운영 규율을 여러 런타임에 뿌릴 때 “한 소스에서만 파생”시켜야 형태마다 내용이 어긋나는 드리프트를 막을 수 있기 때문이다.
그림
flowchart TD
SRC["단일 정본<br/>render_markdown(default_core())"]
SRC --> ALL["export_all(target, base_model)"]
ALL -->|"core_md 1회 렌더 후 공유"| O["export_ollama"]
ALL --> L["export_lmstudio"]
ALL --> S["export_sillytavern"]
ALL --> C["export_claude_plugin"]
O --> OF["ollama/Modelfile<br/>FROM + PARAMETER + SYSTEM"]
L --> LF["lmstudio/lmstudio.preset.json<br/>private_prompt_included=False"]
S --> SF["sillytavern/sillytavern.preset.json<br/>name·description·prompt"]
C --> CF1["claude-plugin/plugin.json"]
C --> CF2["claude-plugin/promptcore.md"]
flowchart LR
T["target 경로"] --> ED["_ensure_dir<br/>mkdir parents=True"]
ED --> W{"형태별 writer"}
W -->|text| WT["_write_text<br/>utf-8"]
W -->|json| WJ["_write_json<br/>ensure_ascii=False, indent=2"]
WT --> P["반환: Path"]
WJ --> P
쉽게 풀기
한 편의 “회사 운영 매뉴얼” 원본이 있다고 하자. 이 매뉴얼을 거래처마다 다른 양식으로 제출해야 한다. 어떤 곳은 워드 파일, 어떤 곳은 양식이 정해진 신청서, 어떤 곳은 폴더에 파일 두 개를 넣은 묶음을 원한다.
가장 위험한 실수는 양식별로 매뉴얼을 따로 손으로 옮겨 적는 것이다. 그러면 한 곳을 고칠 때 다른 곳을 깜빡해서 거래처마다 내용이 달라진다(드리프트). Adapters는 이 문제를 이렇게 푼다.
- 매뉴얼 원본은 딱 한 군데(
_rendered_core())에서만 만든다. export_all은 그 원본을 한 번만 읽어(core_md) 네 양식 함수에 똑같이 건넨다.- 각 양식 함수는 받은 원본을 자기 형식으로 포장만 한다. 내용을 다시 쓰지 않는다.
또 한 가지 안전장치가 있다. LM Studio·Claude plugin 같은 공개 산출물에는 “남의 시스템 프롬프트 전문은 안 들어있다”는 표시(private_prompt_included: False)를 항상 박아 둔다. 빈 값이 들어오면 그냥 넘어가지 않고 에러를 내고 멈춘다(fail-closed). 폴더가 없으면 알아서 만든다(parents=True, exist_ok=True).
핵심 정리
| export 함수 | 산출 파일 | 형식 | 핵심 특징 |
|---|---|---|---|
export_ollama | Modelfile | 텍스트 | FROM+PARAMETER+SYSTEM """...""", base_model 빈값이면 ValueError |
export_lmstudio | lmstudio.preset.json | JSON | metadata.private_prompt_included 항상 False |
export_sillytavern | sillytavern.preset.json | JSON | {name, description, prompt} 3키 |
export_claude_plugin | plugin.json + promptcore.md | JSON+텍스트 | 두 Path 리스트 반환, license: AGPL-3.0 |
export_all | 위 전부 | 혼합 | core 1회 렌더 후 전 export 공유 |
[!note]- Ollama Modelfile에 들어가는 고정 파라미터와 SYSTEM 구조
# FableLayer PromptCore — Ollama Modelfile # 단일 소스: promptcore.render_markdown(default_core()) FROM {base_model} PARAMETER temperature 0.4 PARAMETER num_ctx 8192 SYSTEM """ {rstrip 처리된 rendered core} """
temperature 0.4와num_ctx 8192는 코드에 하드코딩된 고정값이다. SYSTEM 본문은 멀티라인이라 삼중따옴표로 감싸고, 끝의 줄바꿈은system.rstrip("\n")으로 제거한다.
[!note]- 공통 헬퍼 4종 (모두 private)
_rendered_core()—render_markdown(default_core())한 줄. 모든 export의 단일 소스._ensure_dir(target)—Path(target)로 정규화 후mkdir(parents=True, exist_ok=True). 디렉토리 보장._write_text(path, content)— utf-8로 쓰고 Path 반환._write_json(path, payload)—json.dumps(ensure_ascii=False, indent=2)끝에\n붙여 쓰고 Path 반환.
[!note]- 드리프트 차단 메커니즘 — core_md 주입 패턴 각 개별 export 함수는
core_md: str | None = None키워드 인자를 받는다.None이면 스스로_rendered_core()를 호출하고, 값이 오면 그 값을 그대로 쓴다.export_all은core_md = _rendered_core()로 한 번만 렌더한 뒤 네 함수 전부에 같은 문자열을 넘긴다. 그래서 한 번의 export_all 안에서 네 형태는 글자 하나까지 동일한 프롬프트를 공유한다.
실제 예시
# fablelayer/adapters.py — Ollama Modelfile 생성 (발췌)
def export_ollama(target: Path, base_model: str = "llama3", *, core_md: str | None = None) -> Path:
"""Ollama Modelfile 생성: FROM + PARAMETER + SYSTEM=rendered core."""
if not base_model:
raise ValueError("base_model 비어있음 (fail-closed)")
out_dir = _ensure_dir(target)
system = _rendered_core() if core_md is None else core_md
lines = [
"# FableLayer PromptCore — Ollama Modelfile",
"# 단일 소스: promptcore.render_markdown(default_core())",
f"FROM {base_model}",
"",
"PARAMETER temperature 0.4",
"PARAMETER num_ctx 8192",
"",
'SYSTEM """',
system.rstrip("\n"),
'"""',
"",
]
return _write_text(out_dir / "Modelfile", "\n".join(lines))
# fablelayer/adapters.py — LM Studio preset (private_prompt_included 강제 False)
def export_lmstudio(target: Path, *, core_md: str | None = None) -> Path:
out_dir = _ensure_dir(target)
system = _rendered_core() if core_md is None else core_md
payload = {
"preset_name": _PRESET_NAME, # "FableLayer PromptCore"
"system_prompt": system,
"metadata": {
"private_prompt_included": False, # 항상 False
"source": "promptcore.render_markdown(default_core())",
"note": "leaked prompt 전문 미포함. inspired-by 패턴만 반영.",
},
}
return _write_json(out_dir / "lmstudio.preset.json", payload)
# fablelayer/adapters.py — export_all: ONE rendered core 를 전 export 공유
def export_all(target: Path, base_model: str = "llama3") -> list[Path]:
out_dir = _ensure_dir(target)
core_md = _rendered_core() # ONE rendered core, 전 export 공유
paths: list[Path] = []
paths.append(export_ollama(out_dir / "ollama", base_model=base_model, core_md=core_md))
paths.append(export_lmstudio(out_dir / "lmstudio", core_md=core_md))
paths.append(export_sillytavern(out_dir / "sillytavern", core_md=core_md))
paths.extend(export_claude_plugin(out_dir / "claude-plugin", core_md=core_md))
return paths
# 직접 만들어 보는 최소 사용 예 (계약 INTERFACE.md 기준)
from pathlib import Path
from fablelayer.adapters import export_all, export_sillytavern
# 전체 한 방에: dist/ 아래 ollama/ lmstudio/ sillytavern/ claude-plugin/ 생성
written = export_all(Path("dist"), base_model="llama3")
for p in written:
print(p) # 각 산출 파일의 Path
# 한 형태만: 반환은 단일 Path
st = export_sillytavern(Path("dist/st-only"))
요약 & 셀프체크
3줄 요약
- Adapters(FL5)는 단일 정본 프롬프트를 Ollama·LM Studio·SillyTavern·Claude plugin 네 형태로 export하는 변환 레이어다.
- 모든 export는
_rendered_core()하나에서 파생하며,export_all은 core를 1회만 렌더해 전 형태에 공유함으로써 드리프트를 막는다. - 빈 base_model은 ValueError로 막고(fail-closed), 공개 산출물에는
private_prompt_included: False와 출처 메타데이터를 항상 박아 안전 경계를 지킨다.
셀프체크
export_all이 core를 함수마다 다시 렌더하지 않고core_md로 넘기는 이유는 무엇인가?export_claude_plugin의 반환 타입이 다른 export와 다른 점은 무엇이고, 어떤 파일 두 개가 나오는가?export_ollama에서base_model이 빈 문자열이면 어떤 일이 일어나며, 그것을 무엇이라 부르는가?