지식위키

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는 이 문제를 이렇게 푼다.

  1. 매뉴얼 원본은 딱 한 군데(_rendered_core())에서만 만든다.
  2. export_all은 그 원본을 한 번만 읽어(core_md) 네 양식 함수에 똑같이 건넨다.
  3. 각 양식 함수는 받은 원본을 자기 형식으로 포장만 한다. 내용을 다시 쓰지 않는다.

또 한 가지 안전장치가 있다. LM Studio·Claude plugin 같은 공개 산출물에는 “남의 시스템 프롬프트 전문은 안 들어있다”는 표시(private_prompt_included: False)를 항상 박아 둔다. 빈 값이 들어오면 그냥 넘어가지 않고 에러를 내고 멈춘다(fail-closed). 폴더가 없으면 알아서 만든다(parents=True, exist_ok=True).

핵심 정리

export 함수산출 파일형식핵심 특징
export_ollamaModelfile텍스트FROM+PARAMETER+SYSTEM """...""", base_model 빈값이면 ValueError
export_lmstudiolmstudio.preset.jsonJSONmetadata.private_prompt_included 항상 False
export_sillytavernsillytavern.preset.jsonJSON{name, description, prompt} 3키
export_claude_pluginplugin.json + promptcore.mdJSON+텍스트두 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.4num_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_allcore_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와 출처 메타데이터를 항상 박아 안전 경계를 지킨다.

셀프체크

  1. export_all이 core를 함수마다 다시 렌더하지 않고 core_md로 넘기는 이유는 무엇인가?
  2. export_claude_plugin의 반환 타입이 다른 export와 다른 점은 무엇이고, 어떤 파일 두 개가 나오는가?
  3. export_ollama에서 base_model이 빈 문자열이면 어떤 일이 일어나며, 그것을 무엇이라 부르는가?

연결

FL_개요 · _분석축_루브릭