// 타로 3카드 해석 프롬프트·스키마. // web-ui tarot-lab prompt.py/schema.py를 참고해 이 저장소 컨벤션으로 TS 포팅. export type TarotInterpretation = { summary: string; cards: { position: string; card: string; reversed: boolean; interpretation: string; evidence: { card_meaning_used: string; position_logic: string; category_lens: string }; advice: string; }[]; interactions: { type: 'synergy' | 'conflict' | 'transition'; between: string[]; explanation: string }[]; advice: string; warning: string | null; confidence: 'high' | 'medium' | 'low'; }; export const TAROT_SYSTEM_PROMPT = `당신은 라이더-웨이트(RWS) 덱 전통 상징에 기반해 타로를 해석하는 전문가입니다. 해석 원칙: 1. 제공된 참고 블록의 키워드/의미만 근거로 삼습니다. 외부 해석·임의 상징을 도입하지 않습니다. 2. 각 카드의 위치 의미와 카드 의미를 결합하고, evidence에 근거를 남깁니다. 3. 3장 스프레드는 카드 간 상호작용(원소·슈트·메이저 비율의 시너지, 슈트 충돌, 정역 전환)을 분석합니다. 4. 운명을 단정하지 말고 성찰을 돕는 톤으로 씁니다. 5. 카테고리에 따라 강조점을 달리합니다. 6. 사용자의 질문을 evidence와 advice에서 인용합니다. 반드시 코드블록 없이 순수 JSON만 출력합니다. 아래 스키마를 정확히 따릅니다: {"summary","cards":[{"position","card","reversed","interpretation","evidence":{"card_meaning_used","position_logic","category_lens"},"advice"}],"interactions":[{"type":"synergy|conflict|transition","between":[],"explanation"}],"advice","warning","confidence":"high|medium|low"} confidence: 카드들이 일관된 서사면 high, 충돌이 크면 low.`; export function buildTarotUserMessage(input: { spread_type: string; category: string | null; question: string | null; cards_reference: string; context_meta: unknown; }): string { return [ input.question ? `질문: ${input.question}` : '질문: (없음)', input.category ? `카테고리: ${input.category}` : '카테고리: 일반', `스프레드: ${input.spread_type} (3장)`, '--- 카드 참고 블록 ---', input.cards_reference, '--- 맥락 메타 ---', JSON.stringify(input.context_meta), '위 근거만으로 스키마에 맞는 JSON을 생성하세요.', ].join('\n'); } /** 코드블록 스트립 + {...} 추출 후 파싱. 실패 시 null */ export function parseTarotJson(raw: string): TarotInterpretation | null { let text = raw.trim().replace(/^```(json)?/i, '').replace(/```$/, '').trim(); const first = text.indexOf('{'); const last = text.lastIndexOf('}'); if (first >= 0 && last > first) text = text.slice(first, last + 1); try { return JSON.parse(text) as TarotInterpretation; } catch { return null; } } /** 스키마 검증. 통과 못하면 사유 문자열, 통과면 null */ export function validateTarot(obj: unknown, spreadType: string): string | null { if (!obj || typeof obj !== 'object') return 'not an object'; const o = obj as Record; if (typeof o.summary !== 'string' || !o.summary) return 'summary 누락'; if (!Array.isArray(o.cards) || o.cards.length === 0) return 'cards 누락'; for (const c of o.cards as Record[]) { if (typeof c.position !== 'string' || typeof c.card !== 'string') return 'card position/card 누락'; if (typeof c.interpretation !== 'string' || !c.interpretation) return 'card interpretation 누락'; const ev = c.evidence as Record | undefined; if (!ev || !ev.card_meaning_used || !ev.position_logic || !ev.category_lens) return 'evidence 3필드 필요'; if (typeof c.advice !== 'string') return 'card advice 누락'; } if (!Array.isArray(o.interactions)) return 'interactions 누락'; if (spreadType === 'three_card' && (o.interactions as unknown[]).length < 1) return 'three_card interactions ≥1 필요'; if (typeof o.advice !== 'string' || !o.advice) return 'advice 누락'; if (!['high', 'medium', 'low'].includes(o.confidence as string)) return 'confidence enum 오류'; return null; }