# -*- coding: utf-8 -*-
r"""
scribe_forge.py — SHIN 서기관 에이전트 [CEO 설계 2026-07-19]

사상: 대화 자체가 암묵지를 낳는다. 맥락 저장 에이전트(=/api/log, SHIN세션로그)는 대화 원본을 쌓지만,
      그 안의 '중요한 것'을 골라 정리하지 않으면 묻힌다. 서기관은 대화를 읽어
      ①이 대화에서 정해진 것 ②대표가 가르친 사실·노하우 ③새로 나온 수치 ④남은 질문을 뽑아
      옵시디언에 정리한다. 옵시디언은 SHIN 지식 인덱스에 포함되므로 → 곧 SHIN의 영구 기억이 된다.

두 에이전트 역할분담:
  - 맥락 저장 에이전트 = dzw_process.js logShin → 서버 /api/log → SHIN세션로그/<날짜>.jsonl (원본)
  - 서기관 에이전트 = 이 스크립트 (원본 → 중요내용 정리 → 옵시디언)

실행: python scribe_forge.py [YYYY-MM-DD]   (날짜 없으면 가장 최근 로그)
비용: 기본 로컬(무료). --fable 주면 페이블로 품질 우선.
"""
import os, io, re, sys, json, glob, time, subprocess, urllib.request

LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'SHIN세션로그')
OUT_DIR = r'E:\옵시디언\도진웍스\03_AI_OS\_SHIN서기관'
CLAUDE_EXE = os.path.expanduser(r'~\.local\bin\claude.exe')
OLLAMA = 'http://localhost:11434/api/generate'
STATE = os.path.join(OUT_DIR, '_scribe_state.json')

PROMPT = ('너는 도진웍스 SHIN의 서기관이다. 아래는 대표(장인)와 SHIN의 대화 기록이다. '
          '이 대화에서 "나중에 반드시 기억해야 할 것"만 골라 정리하라. 잡담·단순조작·중복은 버려라.\n'
          '규칙(CEO 문서규칙): 서문 2~3문장, 도번·용어엔 쉬운 말 병기, 조건→결론 이야기 순서, '
          '대표 표현을 응축해 뭉개지 말고 살려라. 대화에 실제로 있는 내용만(지어내기 금지). 없으면 그 항목은 비워라.\n'
          '출력(마크다운):\n'
          '# %s 대화 정리 — SHIN 서기관\n> (서문)\n'
          '## 1. 이 대화에서 정해진 것 (결정)\n## 2. 대표가 가르친 사실·노하우\n'
          '## 3. 새로 나온 수치·값\n## 4. 남은 질문 / 확인할 것\n\n[대화 기록]\n%s')


def load_json(p, d):
    try: return json.load(io.open(p, encoding='utf-8'))
    except Exception: return d


def latest_log():
    fs = sorted(glob.glob(os.path.join(LOG_DIR, '*.jsonl')))
    return fs[-1] if fs else None


def read_convo(path):
    turns = []
    for ln in io.open(path, encoding='utf-8', errors='ignore'):
        try: r = json.loads(ln)
        except Exception: continue
        kind = r.get('kind') or r.get('role') or ''
        txt = (r.get('text') or r.get('msg') or '').strip()
        if not txt or len(txt) < 3: continue
        # 순수 콘솔/조작 로그·시스템 잡음 제외, 대화(대표/SHIN)만
        if kind in ('대표', 'SHIN', 'user', 'aria') and not txt.startswith('['):
            turns.append('%s: %s' % ('대표' if kind in ('대표', 'user') else 'SHIN', txt[:500]))
    return turns


def ollama(prompt, timeout=300):
    body = json.dumps({'model': 'qwen3.6:27b' if False else 'deepseek-r1:14b', 'prompt': prompt, 'stream': False,
                       'options': {'temperature': 0.3, 'num_predict': 2000}}).encode()
    r = urllib.request.urlopen(urllib.request.Request(OLLAMA, data=body,
                               headers={'Content-Type': 'application/json'}), timeout=timeout)
    out = json.loads(r.read()).get('response', '')
    return re.sub(r'<think>.*?</think>', '', out, flags=re.S).strip()


def fable(prompt):
    r = subprocess.run([CLAUDE_EXE, '-p', '--model', 'claude-fable-5'], input=prompt.encode('utf-8'),
                       capture_output=True, timeout=200)
    return r.stdout.decode('utf-8', errors='ignore').strip()


def main():
    args = [a for a in sys.argv[1:] if not a.startswith('--')]
    use_fable = '--fable' in sys.argv
    path = None
    if args:
        cand = os.path.join(LOG_DIR, args[0] + '.jsonl')
        path = cand if os.path.exists(cand) else None
    path = path or latest_log()
    if not path:
        print('[서기관] 세션 로그 없음'); return
    day = os.path.basename(path).replace('.jsonl', '')
    turns = read_convo(path)
    if len(turns) < 2:
        print('[서기관] 정리할 대화가 부족합니다 (%d턴)' % len(turns)); return
    # 이미 정리한 턴 수 기록 → 증분만
    st = load_json(STATE, {})
    done = st.get(day, 0)
    if len(turns) <= done:
        print('[서기관] %s — 새 대화 없음(이미 %d턴 정리)' % (day, done)); return
    convo = '\n'.join(turns[-80:])   # 최근 80턴
    prompt = PROMPT % (day, convo[:9000])
    try:
        note = fable(prompt) if (use_fable and os.path.exists(CLAUDE_EXE)) else ollama(prompt)
    except Exception as e:
        print('[서기관] 정리 실패:', str(e)[:100]); return
    os.makedirs(OUT_DIR, exist_ok=True)
    out_p = os.path.join(OUT_DIR, 'SHIN정리_%s.md' % day)
    head = '<!-- SHIN 서기관 자동정리: scribe_forge.py %s | 대화 %d턴 | 옵시디언=SHIN 영구기억 -->\n' % (
        time.strftime('%Y-%m-%d %H:%M'), len(turns))
    io.open(out_p, 'w', encoding='utf-8').write(head + note)
    st[day] = len(turns); io.open(STATE, 'w', encoding='utf-8').write(json.dumps(st, ensure_ascii=False))
    print('[서기관] 정리 완료: %s (%d턴 → 옵시디언). SHIN이 /api/reload 하면 영구기억으로 편입.' % (out_p, len(turns)))


if __name__ == '__main__':
    main()
