import '../models/frame_pattern.dart'; import '../models/habit.dart'; import 'frame_candidate.dart'; /// Builds the prompt string fed to the LLM. Pure — no I/O, no globals. /// /// Strategy: pick top-N FramePattern by token-overlap with rawText. If no /// match, fall back to first [maxFewShot] patterns. If patterns is empty, /// emit a system+user prompt with no few-shot section (graceful). String buildFewShotPrompt( SuggestFrameInput input, List framePatterns, { int maxFewShot = 5, }) { final tokens = _tokenize(input.rawText); final scored = >[]; for (final p in framePatterns) { final score = _scorePattern(tokens, p); if (score > 0) scored.add(MapEntry(p, score)); } scored.sort((a, b) => b.value.compareTo(a.value)); List selected; if (scored.isNotEmpty) { selected = scored.take(maxFewShot).map((e) => e.key).toList(); } else if (framePatterns.isNotEmpty) { selected = framePatterns.take(maxFewShot).toList(); } else { selected = const []; } final buf = StringBuffer(); buf.writeln(_systemPrompt()); if (selected.isNotEmpty) { buf.writeln(); buf.writeln('# 변환 예시'); var i = 1; for (final p in selected) { buf.writeln('## 예시 $i'); buf.writeln('L0: ${p.l0Example}'); buf.writeln('L2: ${p.l2Suggestion}'); if (p.l3Identity != null) { buf.writeln('L3: ${p.l3Identity}'); } buf.writeln(); i += 1; } } buf.writeln('# 사용자 입력'); buf.writeln('habit_type: ${input.habitType.dbValue}'); buf.writeln('raw_text: "${input.rawText}"'); if (input.anchorHint != null && input.anchorHint!.isNotEmpty) { buf.writeln('anchor_hint: "${input.anchorHint}"'); } else { buf.writeln('anchor_hint: 없음'); } buf.writeln(); buf.writeln( '위 raw_text 를 정확히 L2(조건부 긍정) 2개 + L3(정체성) 1개, 총 3개 후보로 ' '변환하세요. 순서는 L2 → L2 → L3. emit_frame_candidates 함수로 호출하세요.', ); return buf.toString(); } String _systemPrompt() => ''' 당신은 Huberman 프로토콜을 따르는 한국어 코치입니다. 사용자가 입력한 raw_text 를 L2 (조건부 긍정 — "X 할 때 Y 한다") 또는 L3 (정체성 — "나는 ~ 인 사람이다") 프레임의 한국어 문장으로 변환합니다. 규칙: - L0/L1 (회피·부정·금지 표현) 금지. "안", "끊다", "그만두다", "참는다" 사용 금지. - 각 후보는 120자 이내. - 의도가 명확하지 않으면 confidence 를 낮춥니다. - 반드시 함수 emit_frame_candidates 를 호출해 JSON 으로 응답합니다.'''; List _tokenize(String text) { final normalized = text.trim(); if (normalized.isEmpty) return const []; // Split on whitespace + Korean punctuation. final parts = normalized.split(RegExp(r'[\s,.!?;:()\[\]"' "'" r'`/\\]+')); return parts.where((t) => t.isNotEmpty).toList(growable: false); } int _scorePattern(List tokens, FramePatternModel p) { if (tokens.isEmpty) return 0; var score = 0; for (final t in tokens) { if (t.contains(p.avoidanceKeyword) || p.avoidanceKeyword.contains(t)) { score += 3; } if (p.domain != null && t.contains(p.domain!)) { score += 1; } } return score; }