jeongkee commited on
Commit
e4c8587
ยท
verified ยท
1 Parent(s): 9e0124c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +684 -0
app.py ADDED
@@ -0,0 +1,684 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Mixed Prompt Composer โ€” Combo + Free-text + Strong Rationale + RAG(FAISS)
3
+ # Gradio 4.x / Hugging Face Spaces ํ˜ธํ™˜
4
+
5
+ import os, re, json
6
+ from typing import List, Dict, Tuple
7
+ import gradio as gr
8
+
9
+ # ===== RAG deps =====
10
+ import faiss
11
+ import numpy as np
12
+ import pandas as pd
13
+ from sentence_transformers import SentenceTransformer
14
+ from pypdf import PdfReader
15
+ from docx import Document as Docx
16
+
17
+ # =============== ๋ชจ๋ธ ์›Œ๋ฐ์—…(์ตœ์ดˆ ๋กœ๋”ฉ ์ง€์—ฐ ์™„ํ™”) ===============
18
+ EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
19
+ try:
20
+ _WARMUP = SentenceTransformer(EMBED_MODEL_NAME)
21
+ except Exception:
22
+ _WARMUP = None
23
+
24
+ # -----------------------------
25
+ # 0) ๋ฐ์ดํ„ฐ์…‹/๊ธ€๋กœ์„œ๋ฆฌ
26
+ # -----------------------------
27
+ ALL_TECHS = [
28
+ "Persona Prompting","Few-shot Prompting","Self-consistency Prompting","Output Formatting",
29
+ "Chain-of-Thought (CoT)","Constrained Prompting","RAG Prompting","Step-back Prompting","Role Prompting",
30
+ ]
31
+
32
+ TECH_GLOSSARY = {
33
+ "Persona Prompting": {
34
+ "desc": "๋Œ€์ƒ ์—ญํ• /์„ธ๊ทธ๋จผํŠธ์˜ ์–ธ์–ดยทKPIยท๊ด€์‹ฌ์‚ฌ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ •๋ ฌํ•ด ๋ฐ˜์‘๋ฅ ยท๊ณต๊ฐ๋„๋ฅผ ๋†’์ž„.",
35
+ "purpose": "๋ˆ„๊ตฌ์—๊ฒŒ ๋งํ•˜๋Š”์ง€ ๋ถ„๋ช…ํžˆ ํ•ด ๊ฐ€์น˜๊ฐ€ โ€˜๊ทธ๋“ค์˜ ์–ธ์–ดโ€™๋กœ ์ „๋‹ฌ๋˜๊ฒŒ ํ•จ.",
36
+ "mechanics": [
37
+ "์—ญํ• /๊ถŒํ•œ/๊ด€์‹ฌ KPI ๋ช…์‹œ(์˜ˆ: Sales Leader=ํŒŒ์ดํ”„๋ผ์ธยท์Šน๋ฅ ยท๋ฆฌ๋“œํƒ€์ž„).",
38
+ "ํ†ค/๊ธˆ์น™์–ด/์„ ํ˜ธ ํ‘œํ˜„ ์ •์˜(์ง์„คยท๊ฐ„๊ฒฐยท์ˆซ์ž/ROI, ๋ชจํ˜ธ์–ด ๊ธˆ์ง€).",
39
+ "ํŽ˜๋ฅด์†Œ๋‚˜๋ณ„ ๋ฌธ์žฅ ๋งคํ•‘(๋ฌธ์ œโ†’๊ฐ€์น˜โ†’์ฆ๊ฑฐโ†’CTA)."
40
+ ],
41
+ "example": "์˜ˆ) โ€œ์ด๋ฒˆ ๋ถ„๊ธฐ ํŒŒ์ดํ”„๋ผ์ธ 18% ๋ณด๊ฐ• ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ๋น ๋ฅธ ์•ก์…˜ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค.โ€"
42
+ },
43
+ "Few-shot Prompting": {
44
+ "desc": "์†Œ์ˆ˜์˜ ๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ๋ฅผ ์ œ๊ณตํ•ด ํ†คยท๊ตฌ์กฐยท๋ฆฌ๋“ฌ์„ ๋ชจ์‚ฌ, ํ’ˆ์งˆ ํŽธ์ฐจโ†“ยท์†๋„โ†‘.",
45
+ "purpose": "๊ฒ€์ฆ๋œ ํŒจํ„ด ๋ณต์ œ๋กœ ์ผ๊ด€์„ฑ ํ™•๋ณด.",
46
+ "mechanics": [
47
+ "์ƒ˜ํ”Œ 2~3๊ฐœ๋ฅผ ํ—ค๋“œ๋ผ์ธ/์˜คํ”„๋‹/๊ทผ๊ฑฐ/CTA ํŒจํ„ด์œผ๋กœ ์ œ๊ณต.",
48
+ "โ€˜์ด ํ†ค์„ ๋ชจ์‚ฌํ•ด 3๊ฐœ ๋ณ€ํ˜•โ€™์ฒ˜๋Ÿผ ๋‹ค๋ณ€๋Ÿ‰ ํ›„๋ณด ์ƒ์„ฑ."
49
+ ],
50
+ "example": "[์ƒ˜ํ”Œ] {์‚ฐ์—…} ํŒ€๋“ค์˜ {์„ฑ๊ณผ}(์‚ฌ๋ก€). ์ด๋ฒˆ์ฃผ {์ˆ˜/๋ชฉ} {11:00/16:00} 15๋ถ„ ํ†ตํ™” ๊ฐ€๋Šฅํ•˜์‹ค๊นŒ์š”?"
51
+ },
52
+ "Self-consistency Prompting": {
53
+ "desc": "์—ฌ๋Ÿฌ ํ›„๋ณด์•ˆ ์ƒ์„ฑ ํ›„ ์ž์ฒด ์ฑ„์ (์ฒดํฌ๋ฆฌ์ŠคํŠธ/์Šค์ฝ”์–ด๋ง)์œผ๋กœ ์ตœ์ ์•ˆ ์„ ํƒ.",
54
+ "purpose": "A/B ํ…Œ์ŠคํŠธ ๋‚ด์žฅ์œผ๋กœ ํ’ˆ์งˆ ํ–ฅ์ƒ.",
55
+ "mechanics": [
56
+ "์ œ๋ชฉ5ยท๋ฐ”๋””3ยทCTA3 ๋“ฑ ๋‹ค๋ณ€๋Ÿ‰ ์ƒ์„ฑ.",
57
+ "์ฒดํฌ๋ฆฌ์ŠคํŠธ(๊ฐœ์ธํ™”/๋ช…๋ฃŒ/๊ฐ€์น˜/์ŠคํŒธํšŒํ”ผ/CTA) ์ฑ„์ โ†’Top-1 ์ถœ๋ ฅ."
58
+ ],
59
+ "example": "์ถœ๋ ฅ: ํ›„๋ณด ๋ฆฌ์ŠคํŠธ + ์ฑ„์ ํ‘œ + ์ตœ์ข… ์ถ”์ฒœ 1~2์•ˆ."
60
+ },
61
+ "Output Formatting": {
62
+ "desc": "์‚ฐ์ถœ๋ฌผ์˜ ํ˜•์‹/์„น์…˜/ํ•„๋“œ ๊ณ ์ •(ํ‘œ/์„น์…˜/JSON ๋“ฑ)์œผ๋กœ ์†๋„ยท๊ฐ€๋…์„ฑยท์žฌ์‚ฌ์šฉ์„ฑ ํ™•๋ณด.",
63
+ "purpose": "์Šน์ธ ๋ฃจํ”„ ๋‹จ์ถ•, ํ‘œ์ค€ํ™”.",
64
+ "mechanics": [
65
+ "์œ ํ˜•๋ณ„ ํ…œํ”Œ๋ฆฟ ๊ฐ•์ œ(์ด๋ฉ”์ผ/์นดํ”ผ/๋ณด๊ณ ์„œ/PRD/API).",
66
+ "ํ•„์ˆ˜ยท์„ ํƒ ํ•„๋“œ/๊ธธ์ด/๊ธˆ์น™์–ด ๋ช…์‹œ."
67
+ ],
68
+ "example": "์ด๋ฉ”์ผ: ์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS"
69
+ },
70
+ "Chain-of-Thought (CoT)": {
71
+ "desc": "๋ชฉํ‘œโ†’์ง€ํ‘œโ†’๋Œ€์•ˆโ†’์„ ํƒโ†’์‹คํ–‰์˜ ๋‹จ๊ณ„์  ์ถ”๋ก ์œผ๋กœ ๋…ผ๋ฆฌ ๋น„์•ฝ ์ตœ์†Œํ™”.",
72
+ "purpose": "๋ถ„์„/์ „๋žต ๋ฌธ์„œ์˜ ๋…ผ๋ฆฌ ์ผ๊ด€์„ฑ.",
73
+ "mechanics": ["๊ฐ ๋‹จ๊ณ„์— ๊ฐ€์ •/๊ทผ๊ฑฐ/๋Œ€์•ˆ/๋ฆฌ์Šคํฌ/๊ถŒ๊ณ  ์ฒดํฌ ์งˆ๋ฌธ ๋ถ€์—ฌ."],
74
+ "example": "์š”์•ฝโ†’ํ˜„ํ™ฉโ†’๊ณผ์ œโ†’๋ถ„์„โ†’์ธ์‚ฌ์ดํŠธโ†’๊ถŒ๊ณ โ†’ํ•œ๊ณ„"
75
+ },
76
+ "Constrained Prompting": {
77
+ "desc": "๊ธธ์ดยท๊ธˆ์น™์–ดยทํ•„์ˆ˜ํ•„๋“œยทJSON ์Šคํ‚ค๋งˆ ๋“ฑ ์ œ์•ฝ ์ค€์ˆ˜.",
78
+ "purpose": "๋ธŒ๋žœ๋“œ/๋ฒ•๋ฌด/์‹ฌ์˜ ๋ฆฌ์Šคํฌ ์ตœ์†Œํ™”.",
79
+ "mechanics": ["์ œ๋ชฉ โ‰ค 30์ž, ๊ธˆ์น™์–ด ์ œ๊ฑฐ, ํ•„์ˆ˜ ํ‚ค ๋ˆ„๋ฝ ์‹œ ์žฌ์ƒ์„ฑ."],
80
+ "example": "JSON ์Šคํ‚ค๋งˆ ์ค€์ˆ˜ ์ถœ๋ ฅ / ํ‘œ์ค€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํฌํ•จ"
81
+ },
82
+ "RAG Prompting": {
83
+ "desc": "์™ธ๋ถ€ ๋ณด๊ณ ์„œ/DB/๋ฌธ์„œ์˜ ๊ทผ๊ฑฐ ์ฃผ์ž…์œผ๋กœ ์ตœ์‹ ์„ฑยท์‹ ๋ขฐ์„ฑ ํ™•๋ณด.",
84
+ "purpose": "์ถ”์ •/ํ™˜๊ฐ ๋ฐฉ์ง€, ์ถœ์ฒ˜ ๊ธฐ๋ฐ˜ ์„œ์ˆ .",
85
+ "mechanics": ["โ€˜๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ ์ถ”์ • ๊ธˆ์ง€, ์ถœ์ฒ˜ ๋ฉ”๋ชจโ€™ ์ง€์‹œ.", "์ธ์šฉ/๊ฐ์ฃผ/๋งํฌ ํ‘œ๊ธฐ."],
86
+ "example": "์˜ˆ) Gartner MQ 2024, ๊ณต์‹œ 2024Q3, Crunchbase 2024.07"
87
+ },
88
+ "Step-back Prompting": {
89
+ "desc": "์ƒ์œ„ ๋ชฉ์ /์›์น™์—์„œ ์ถœ๋ฐœํ•ด ์˜๋ฏธ ์ค‘์‹ฌ์œผ๋กœ ์žฌํ•ด์„.",
90
+ "purpose": "โ€˜์™œ ์ค‘์š”ํ•œ๊ฐ€โ€™์— ๋จผ์ € ๋‹ตํ•ด ๊ฒฝ์˜ ์‹œ์‚ฌ์  ๊ฐ•ํ™”.",
91
+ "mechanics": ["์ƒ์œ„ ๋ชฉํ‘œโ†’ํ•ต์‹ฌ ์›์น™โ†’ํ˜„์žฌ ์„ ํƒ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ."],
92
+ "example": "โ€˜๋น„์šฉ 20%โ†“โ€™๊ฐ€ ์ „๋žต KPI์— ์–ด๋–ป๊ฒŒ ๊ธฐ์—ฌํ•˜๋Š”์ง€ ์—ฐ๊ฒฐ"
93
+ },
94
+ "Role Prompting": {
95
+ "desc": "โ€˜๋‹น์‹ ์€ PM/์ „๋žต๊ฐ€/UX ๋ผ์ดํ„ฐโ€ฆโ€™์ฒ˜๋Ÿผ ๊ด€์  ๊ณ ์ •์œผ๋กœ ๋ชฉ์  ์ ํ•ฉ์„ฑโ†‘.",
96
+ "purpose": "์ง๋ฌด๋ณ„ ์–ธ์–ด/ํŒ๋‹จ ๊ธฐ์ค€ ์ผ์น˜.",
97
+ "mechanics": ["์—ญํ• ยทKPIยท๊ฒฐ์ •๊ถŒยท๋ฆฌ์Šคํฌ ๊ด€์  ๋ช…์‹œ."],
98
+ "example": "PM: ๋ฌธ์ œ์ •์˜/AC/๋ฆฌ์Šคํฌ | ์ „๋žต: ์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /๊ฑฐ๋ฒ„๋„Œ์Šค"
99
+ },
100
+ }
101
+
102
+ CATALOG = {
103
+ "1 ์™ธ๋ถ€ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜": {
104
+ "subdomains": ["์ด๋ฉ”์ผ(์ฝœ๋“œ/์›œ)", "๊ด‘๊ณ /๋žœ๋”ฉ", "PR/๋ณด๋„์ž๋ฃŒ", "SNS/์˜์ƒ"],
105
+ "pains": ["๋ฐ˜์‘๋ฅ  ์ €์กฐ", "๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜", "์ž‘์„ฑ ์‹œ๊ฐ„ ๊ณผ๋‹ค", "A/B ํ…Œ์ŠคํŠธ ๋ถ€๋‹ด"],
106
+ "outputs": ["์ฝœ๋“œ๋ฉ”์ผ", "๊ด‘๊ณ  ์นดํ”ผ", "๋ณด๋„์ž๋ฃŒ", "๋žœ๋”ฉํŽ˜์ด์ง€ ์„น์…˜"],
107
+ "users": ["์„ธ์ผ์ฆˆ", "๋งˆ์ผ€ํŒ…ํŒ€", "PRํŒ€", "์ฐฝ์—…์ž"]
108
+ },
109
+ "2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜": {
110
+ "subdomains": ["์‹œ์žฅ๋ณด๊ณ ์„œ", "๊ฒฝ์Ÿ๋ถ„์„", "VOC ๋ถ„์„", "ํŽ˜๋ฅด์†Œ๋‚˜"],
111
+ "pains": ["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ", "๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™•", "์ถœ์ฒ˜/๊ธฐ๊ฐ„ ๋ˆ„๋ฝ", "๋ฆฌ์„œ์น˜ ์ž‘์„ฑ ๋ถ€๋‹ด"],
112
+ "outputs": ["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ", "๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„", "ํŽ˜๋ฅด์†Œ๋‚˜", "VOC ์š”์•ฝ"],
113
+ "users": ["์ „๋žตํŒ€", "๊ธฐํšํŒ€", "์ปจ์„คํ„ดํŠธ", "IR/ํˆฌ์ž์ค€๋น„ํŒ€"]
114
+ },
115
+ "3 ์ œํ’ˆยทUX ๋ฌธ์„œ": {
116
+ "subdomains": ["PRD/์š”๊ตฌ์‚ฌํ•ญ", "์œ ์Šค์ผ€์ด์Šค", "UX ๋งˆ์ดํฌ๋กœ์นดํ”ผ", "๋ฆด๋ฆฌ์Šค ๋…ธํŠธ"],
117
+ "pains": ["์š”๊ตฌ์‚ฌํ•ญ ์–ธ์–ดํ™” ๋‚œํ•ญ", "UX ์นดํ”ผ ๋ถˆ์ผ์น˜", "์˜์‚ฌ๊ฒฐ์ • ๊ธฐ์ค€ ๋ถˆ๋ช…ํ™•"],
118
+ "outputs": ["PRD", "์œ ์Šค์ผ€์ด์Šค", "UX ์นดํ”ผ", "๋ฆด๋ฆฌ์Šค ๋…ธํŠธ"],
119
+ "users": ["PM", "UX ๋””์ž์ด๋„ˆ", "๊ฐœ๋ฐœํŒ€", "QA"]
120
+ },
121
+ }
122
+
123
+ PAIN_SUB = {
124
+ "๋ฐ˜์‘๋ฅ  ์ €์กฐ": ["์ œ๋ชฉ/ํ”„๋ฆฌํ—ค๋”", "ํƒ€๊ฒŒํŒ…", "CTA ์•ฝํ•จ", "์‹ ๋ขฐ ๊ทผ๊ฑฐ ๋ถ€์กฑ"],
125
+ "๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜": ["ํ†ค/๋ณด์ด์Šค", "ํฌ๋งท/๊ธธ์ด", "์ฑ„๋„/ํŽ˜์ด์‹ฑ"],
126
+ "์ž‘์„ฑ ์‹œ๊ฐ„ ๊ณผ๋‹ค": ["ํ…œํ”Œ๋ฆฟ ๋ถ€์žฌ", "์˜ˆ์‹œ ๋ถ€์กฑ", "์Šน์ธ ๋ฃจํ”„ ์ง€์—ฐ"],
127
+ "๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ": ["์ž๋ฃŒ ์ˆ˜์ง‘", "์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ", "์ตœ์‹ ์„ฑ"],
128
+ "๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™•": ["์„ธ๊ทธ๋จผํŠธ ์ •์˜", "JTBD ๋ชจํ˜ธ", "ํŽ˜์ธ/๊ฒŒ์ธ"],
129
+ }
130
+
131
+ OUTPUT_SUB = {
132
+ "์ฝœ๋“œ๋ฉ”์ผ": ["์‹ ๊ทœ ์ธ๋ฐ”์šด๋“œ", "์ฝœ๋“œ ์•„์›ƒ๋ฐ”์šด๋“œ", "ํ›„์†/๋ฆฌ๋งˆ์ธ๋“œ", "์ดํƒˆ ์žฌ์ฐธ์—ฌ"],
133
+ "๊ด‘๊ณ  ์นดํ”ผ": ["์›น ๋ฐฐ๋„ˆ", "๊ฒ€์ƒ‰๊ด‘๊ณ ", "SNS ์นด๋“œ", "์•ฑ ํ‘ธ์‹œ"],
134
+ "์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ": ["ํƒ‘๋‹ค์šด(ํƒ์ƒ‰)", "๋ฐ”ํ…€์—… ์ถ”์ •", "ํ˜ผํ•ฉ ์ ‘๊ทผ", "๋ฆฌ์„œ์น˜ ๋ธŒ๋ฆฌํ”„"],
135
+ "๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„": ["๊ธฐ๋Šฅ ๋น„๊ตํ‘œ", "๊ฐ€๊ฒฉ/ํŒจํ‚ค์ง€", "ํฌ์ง€์…”๋‹ ๋งต", "SWOT"],
136
+ "PRD": ["๋ฌธ์ œ์ •์˜", "๋ชฉํ‘œ/์ง€ํ‘œ", "๋ฒ”์œ„/๋น„๋ฒ”์œ„", "์ˆ˜์šฉ๊ธฐ์ค€(AC)", "๋ฆฌ์Šคํฌ"],
137
+ }
138
+
139
+ USER_SUB = {
140
+ "์„ธ์ผ์ฆˆ": ["BDR", "AE", "AM", "Sales Leader"],
141
+ "๋งˆ์ผ€ํŒ…ํŒ€": ["ํผํฌ๋จผ์Šค", "์ฝ˜ํ…์ธ ", "๋ธŒ๋žœ๋“œ", "๊ทธ๋กœ์Šค"],
142
+ "PRํŒ€": ["์ฝ”ํผ๋ ˆ์ดํŠธ", "ํ”„๋กœ๋•ํŠธ PR"],
143
+ "์ฐฝ์—…์ž": ["Seed", "Series A+", "๋ถ€ํŠธ์ŠคํŠธ๋žฉ"],
144
+ "์ „๋žตํŒ€": ["Corp Strategy", "Biz Ops"],
145
+ "PM": ["Jr PM", "Sr PM", "Group PM"],
146
+ "UX ๋””์ž์ด๋„ˆ": ["UX Writer", "Product Designer"],
147
+ "๊ฐœ๋ฐœํŒ€": ["FE", "BE", "ML", "Infra"],
148
+ "QA": ["QA ์—”์ง€๋‹ˆ์–ด", "QA ๋ฆฌ๋“œ"]
149
+ }
150
+
151
+ # -----------------------------
152
+ # ์œ ํ‹ธ
153
+ # -----------------------------
154
+ def uniq(xs: List[str]) -> List[str]:
155
+ if not xs: return []
156
+ s=set(); out=[]
157
+ for x in xs:
158
+ if x not in s:
159
+ s.add(x); out.append(x)
160
+ return out
161
+
162
+ AUTO_RULES_PAIN = {
163
+ "๋ฐ˜์‘๋ฅ ": ["Persona Prompting", "Few-shot Prompting", "Self-consistency Prompting"],
164
+ "๋ถˆ์ผ์น˜": ["Output Formatting", "Constrained Prompting"],
165
+ "์ž‘์„ฑ": ["Output Formatting", "Few-shot Prompting"],
166
+ "๊ฒฝ์Ÿ": ["RAG Prompting", "Chain-of-Thought (CoT)"],
167
+ "์š”๊ตฌ": ["Persona Prompting", "Step-back Prompting"],
168
+ }
169
+ AUTO_RULES_OUTPUT = {
170
+ "์ด๋ฉ”์ผ": ["Persona Prompting", "Output Formatting", "Few-shot Prompting", "Self-consistency Prompting"],
171
+ "์นดํ”ผ": ["Few-shot Prompting", "Self-consistency Prompting", "Output Formatting"],
172
+ "๋ณด๊ณ ์„œ": ["RAG Prompting", "Chain-of-Thought (CoT)", "Output Formatting"],
173
+ "PRD": ["Role Prompting", "Constrained Prompting", "Chain-of-Thought (CoT)"],
174
+ }
175
+ AUTO_RULES_USER = {
176
+ "์„ธ์ผ์ฆˆ": ["Persona Prompting", "Few-shot Prompting", "Self-consistency Prompting"],
177
+ "๋งˆ์ผ€ํŒ…": ["Few-shot Prompting", "Self-consistency Prompting"],
178
+ "์ „๋žต": ["Chain-of-Thought (CoT)", "Step-back Prompting", "RAG Prompting"],
179
+ "PM": ["Role Prompting", "Constrained Prompting"],
180
+ "๋ฐ์ดํ„ฐ": ["RAG Prompting", "Constrained Prompting"],
181
+ }
182
+
183
+ def auto_recommend(domain_key, pains, outs, users):
184
+ rec=[]
185
+ if domain_key=="1 ์™ธ๋ถ€ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜":
186
+ rec+=["Persona Prompting","Few-shot Prompting","Self-consistency Prompting","Output Formatting"]
187
+ if domain_key=="2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜":
188
+ rec+=["RAG Prompting","Chain-of-Thought (CoT)","Output Formatting","Step-back Prompting"]
189
+ if domain_key=="3 ์ œํ’ˆยทUX ๋ฌธ์„œ":
190
+ rec+=["Role Prompting","Constrained Prompting","Chain-of-Thought (CoT)","Output Formatting"]
191
+ for p in pains or []:
192
+ for k,ts in AUTO_RULES_PAIN.items():
193
+ if k in p: rec+=ts
194
+ for o in outs or []:
195
+ for k,ts in AUTO_RULES_OUTPUT.items():
196
+ if k in o: rec+=ts
197
+ for u in users or []:
198
+ for k,ts in AUTO_RULES_USER.items():
199
+ if k in u: rec+=ts
200
+ rec.append("Output Formatting")
201
+ return uniq(rec)
202
+
203
+ def guess_format_hint(outs: List[str], override: str="") -> str:
204
+ if override.strip():
205
+ return override.strip()
206
+ j=" ".join(outs or [])
207
+ if any(k in j for k in ["PRD","API","ADR","์ •์ฑ…","SOP","์œ ์Šค์ผ€์ด์Šค","FAQ"]): return "JSON/ํ‘œ/๋ถˆ๋ฆฟ(ํ•„๋“œ ํ‚ค ๊ณ ์ •)"
208
+ if any(k in j for k in ["๋ณด๊ณ ์„œ","๋ธŒ๋ฆฌํ”„","์š”์•ฝ","๋ฌธ์„œ","1-Pager"]): return "์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„"
209
+ if any(k in j for k in ["์นดํ”ผ","๊ด‘๊ณ ","๋žœ๋”ฉ"]): return "ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒํ—ค๋“œ/๋ฐ”๋””/CTA"
210
+ if any(k in j for k in ["์ด๋ฉ”์ผ","๋ฉ”์ผ","์ฝœ๋“œ๋ฉ”์ผ"]): return "์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS"
211
+ return "๋ชฉ์ฐจ/์š”์•ฝ/๋ณธ๋ฌธ/๊ถŒ๊ณ /CTA"
212
+
213
+ # -----------------------------
214
+ # Rationale(๊ฐ•ํ™”ํŒ)
215
+ # -----------------------------
216
+ def reason_from_pain(tech: str, pains: List[str], pain_subs: List[str]) -> List[str]:
217
+ R=[]
218
+ jp=" ".join(pains or []) + " " + " ".join(pain_subs or [])
219
+ if "๋ฐ˜์‘๋ฅ " in jp:
220
+ if tech=="Persona Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ์„ธ๊ทธ๋จผํŠธ ๋งž์ถค ์–ดํœ˜/์–ด์กฐ๋กœ ์ฒด๊ฐ ๊ฐ€์น˜ ์ƒ์Šน")
221
+ if tech=="Few-shot Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ ํŒจํ„ด ๋ณต์ œ")
222
+ if tech=="Self-consistency Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ๋‹ค๋ณ€๋Ÿ‰ ํ›„๋ณด ์ƒ์„ฑโ†’์ž์ฒด ์ฑ„์ ์œผ๋กœ Top-1")
223
+ if "๋ถˆ์ผ์น˜" in jp or "ํ†ค/๋ณด์ด์Šค" in jp or "ํฌ๋งท" in jp:
224
+ if tech=="Output Formatting": R.append("๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜ โ†’ ํ˜•์‹/์„น์…˜ ๊ณ ์ •์œผ๋กœ ์ •ํ•ฉ์„ฑโ†‘")
225
+ if tech=="Constrained Prompting": R.append("๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜ โ†’ ๊ธˆ์น™/๊ธธ์ด/ํ•„์ˆ˜ํ•„๋“œ ๊ฐ•์ œ")
226
+ if "์ž‘์„ฑ ์‹œ๊ฐ„" in jp or "์˜ˆ์‹œ ๋ถ€์กฑ" in jp:
227
+ if tech in {"Few-shot Prompting","Output Formatting"}: R.append("์ž‘์„ฑ์‹œ๊ฐ„ ๊ณผ๋‹ค/์˜ˆ์‹œ ๋ถ€์กฑ โ†’ ํ…œํ”Œ๋ฆฟ + ์˜ˆ์‹œ ๊ธฐ๋ฐ˜ ์†๋„โ†‘")
228
+ if "๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด" in jp:
229
+ if tech=="RAG Prompting": R.append("๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ โ†’ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ๊ทผ๊ฑฐ ์ฃผ์ž…(์ตœ์‹ ์„ฑ/์‹ ๋ขฐ์„ฑ)")
230
+ if tech=="Chain-of-Thought (CoT)": R.append("๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„ ๊ตฌ์กฐํ™”(CoT)๋กœ ์ธ์‚ฌ์ดํŠธ ๋ช…๋ฃŒํ™”")
231
+ if "์š”๊ตฌ ๋ถˆ๋ช…ํ™•" in jp:
232
+ if tech in {"Persona Prompting","Step-back Prompting"}: R.append("๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™• โ†’ ์ƒ์œ„ ๋ชฉ์ /JTBD ์ •๋ ฌ")
233
+ return R
234
+
235
+ def reason_from_output(tech: str, outs: List[str], out_subs: List[str]) -> List[str]:
236
+ R=[]
237
+ jo=" ".join(outs or []) + " " + " ".join(out_subs or [])
238
+ if "๋ฉ”์ผ" in jo or "์นดํ”ผ" in jo or "๋žœ๋”ฉ" in jo:
239
+ if tech=="Output Formatting": R.append("์นดํ”ผ/๋ฉ”์ผ โ†’ ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒ/๋ฐ”๋””/CTA ๊ณ ์ •์ด ์„ฑ๊ณผ ์ขŒ์šฐ")
240
+ if tech=="Self-consistency Prompting": R.append("๋ฉ”์‹œ์ง€ ํ›„๋ณด ๋‹ค๋ณ€๋Ÿ‰ ์ƒ์„ฑโ†’์ตœ์ ์•ˆ ์„ ํƒ ํ•„์š”")
241
+ if tech=="Few-shot Prompting": R.append("์ฑ„๋„๋ณ„ ํ†ค/๊ธธ์ด ์ฐจ์ด๋ฅผ ์˜ˆ์‹œ๋กœ ๋น ๋ฅด๊ฒŒ ์ ์‘")
242
+ if "๋ณด๊ณ ์„œ" in jo or "๋ถ„์„" in jo:
243
+ if tech=="RAG Prompting": R.append("๋ณด๊ณ ์„œ/๋ถ„์„ โ†’ ์ˆ˜์น˜ยท์ถœ์ฒ˜ ์ตœ์‹ ์„ฑ ๋ณด์žฅ")
244
+ if tech=="Chain-of-Thought (CoT)": R.append("๋ณด๊ณ ์„œ/๋ถ„์„ โ†’ ๋‹จ๊ณ„์  ๊ตฌ์กฐ(CoT)๋กœ ๋…ผ๋ฆฌ ๊ฐ•ํ™”")
245
+ if "PRD" in jo or "API" in jo:
246
+ if tech in {"Constrained Prompting","Role Prompting"}: R.append("PRD/API โ†’ JSON/ํ•„๋“œ ๊ฐ•์ œ + ์ง๋ฌด ๊ด€์  ๊ณ ์ • ํ•„์š”")
247
+ return R
248
+
249
+ def reason_from_user(tech: str, users: List[str], user_subs: List[str]) -> List[str]:
250
+ R=[]
251
+ ju=" ".join(users or []) + " " + " ".join(user_subs or [])
252
+ if "์„ธ์ผ์ฆˆ" in ju:
253
+ if tech=="Persona Prompting": R.append("Sales๋Š” ํŒŒ์ดํ”„๋ผ์ธ/์Šน๋ฅ  ์–ธ์–ด ์„ ํ˜ธ โ†’ ํŽ˜๋ฅด์†Œ๋‚˜ ํ†ค ์ ์šฉ")
254
+ if tech=="Few-shot Prompting": R.append("์˜์—… ๋ ˆํผ๋Ÿฐ์Šค ์‚ฌ๋ก€ ๋ชจ์‚ฌ๊ฐ€ ์„ค๋“๋ ฅโ†‘")
255
+ if "์ „๋žต" in ju:
256
+ if tech in {"Chain-of-Thought (CoT)","Step-back Prompting","RAG Prompting"}:
257
+ R.append("์ „๋žต์กฐ์ง์€ ๊ทผ๊ฑฐ/๋…ผ๋ฆฌ/์‹œ์‚ฌ์  ์ค‘์‹œ โ†’ CoT+RAG+Step-back ์ ํ•ฉ")
258
+ if "PM" in ju or "UX" in ju:
259
+ if tech in {"Role Prompting","Constrained Prompting"}:
260
+ R.append("PM/UX๋Š” ํ•„๋“œ/AC/ํ†ค ํ‘œ์ค€ ํ•„์š” โ†’ ์—ญํ•  ๊ณ ์ • + ์ œ์•ฝ ๊ฐ•์ œ")
261
+ return R
262
+
263
+ def domain_reco(tech: str, outs: List[str]) -> str:
264
+ j=" ".join(outs or [])
265
+ if tech=="Output Formatting":
266
+ if "๋ฉ”์ผ" in j: return "์ด๋ฉ”์ผ: ์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS ํ˜•์‹ ๊ณ ์ •"
267
+ if "์นดํ”ผ" in j or "๋žœ๋”ฉ" in j: return "์นดํ”ผ/๋žœ๋”ฉ: ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒ/๋ฐ”๋””/CTA ๋ธ”๋ก ๊ณ ์ •"
268
+ if "๋ณด๊ณ ์„œ" in j: return "๋ณด๊ณ ์„œ: ์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„ ์„น์…˜ ๊ณ ์ •"
269
+ if "PRD" in j: return "PRD: ํ•„์ˆ˜ ํ•„๋“œ(JSON/ํ‘œ) ๊ฐ•์ œ"
270
+ return "์‚ฐ์ถœ๋ฌผ ํ‘œ์ค€ ์„น์…˜ ๊ณ ์ •"
271
+ if tech=="Persona Prompting": return "์„ธ๊ทธ๋จผํŠธยท์—ญํ•  KPI ์–ธ์–ด ์ •๋ ฌ"
272
+ if tech=="Few-shot Prompting": return "๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ ๊ตฌ์กฐ/ํ†ค ๋ชจ์‚ฌ"
273
+ if tech=="Self-consistency Prompting": return "๋‹ค์ค‘ ํ›„๋ณด ์ƒ์„ฑโ†’์ž์ฒด ๏ฟฝ๏ฟฝ๏ฟฝ์ โ†’Top-1"
274
+ if tech=="Constrained Prompting": return "๊ธธ์ด/๊ธˆ์น™์–ด/ํ•„์ˆ˜ํ•„๋“œ ๊ฐ•์ œ"
275
+ if tech=="RAG Prompting": return "์™ธ๋ถ€ ๋ฆฌํฌํŠธ/๋ฐฑ์„œ ๊ทผ๊ฑฐ ์ฃผ์ž…"
276
+ if tech=="Chain-of-Thought (CoT)": return "๋ชฉํ‘œโ†’์ง€ํ‘œโ†’๋Œ€์•ˆโ†’๊ถŒ๊ณ  ๋‹จ๊ณ„ํ™”"
277
+ if tech=="Step-back Prompting": return "์ƒ์œ„ ๋ชฉ์ /์›์น™์—์„œ ์˜๋ฏธ ์žฌํ•ด์„"
278
+ if tech=="Role Prompting": return "์—ญํ•  ๊ณ ์ •์œผ๋กœ ๊ด€์  ์ผ์น˜"
279
+ return "-"
280
+
281
+ def build_rationale_detailed(
282
+ tech: str, domain_key: str, subdomains: List[str],
283
+ pains: List[str], pain_subs: List[str],
284
+ outs: List[str], out_subs: List[str],
285
+ users: List[str], user_subs: List[str]
286
+ ) -> str:
287
+ if not tech: return ""
288
+ g = TECH_GLOSSARY.get(tech, {})
289
+ desc, purpose, mechs, example = g.get("desc",""), g.get("purpose",""), g.get("mechanics",[]), g.get("example","")
290
+
291
+ R = []
292
+ R += reason_from_pain(tech, pains, pain_subs)
293
+ R += reason_from_output(tech, outs, out_subs)
294
+ R += reason_from_user(tech, users, user_subs)
295
+ R = uniq(R)
296
+
297
+ parts = [
298
+ f"## {tech}",
299
+ f"**์„ค๋ช…**: {desc}",
300
+ f"- **์ ์šฉ ๋ชฉ์ **: {purpose}" if purpose else "",
301
+ "- **์ž‘๋™ ๋ฐฉ์‹**:\n - " + "\n - ".join(mechs) if mechs else "",
302
+ f"- **๋„๋ฉ”์ธ ๊ถŒ์žฅ**: {domain_reco(tech, outs)}",
303
+ f"- **์„ ์ • ๊ทผ๊ฑฐ(์„ ํƒ ๋ฐ˜์˜)**:\n - " + "\n - ".join(R) if R else "- **์„ ์ • ๊ทผ๊ฑฐ**: (์„ ํƒ ํ•ญ๋ชฉ์— ๋”ฐ๋ผ ์ž๋™ ์ƒ์„ฑ)",
304
+ f"- **์˜ˆ์‹œ**: {example}"
305
+ ]
306
+ return "\n".join([p for p in parts if p.strip()])
307
+
308
+ # -----------------------------
309
+ # RAG: ์—…๋กœ๋“œโ†’์ฒญํ‚นโ†’์ž„๋ฒ ๋”ฉโ†’FAISSโ†’๊ฒ€์ƒ‰
310
+ # -----------------------------
311
+ _model = None
312
+ _faiss = None
313
+ _chunks = [] # [{id, text, meta}]
314
+ _dim = 384
315
+
316
+ def get_model():
317
+ global _model
318
+ if _model is None:
319
+ _model = SentenceTransformer(EMBED_MODEL_NAME)
320
+ return _model
321
+
322
+ def embed_texts(texts: List[str]) -> np.ndarray:
323
+ model = get_model()
324
+ vecs = model.encode(texts, normalize_embeddings=True)
325
+ return np.array(vecs, dtype="float32")
326
+
327
+ def extract_text(path: str) -> Tuple[str, Dict]:
328
+ name = os.path.basename(path)
329
+ ext = os.path.splitext(path)[1].lower()
330
+ meta = {"source": name}
331
+ if ext == ".pdf":
332
+ reader = PdfReader(path)
333
+ pages = []
334
+ for i, p in enumerate(reader.pages):
335
+ try: pages.append(p.extract_text() or "")
336
+ except: pages.append("")
337
+ return "\n".join(pages), meta
338
+ if ext == ".docx":
339
+ d = Docx(path)
340
+ return "\n".join(p.text for p in d.paragraphs), meta
341
+ if ext == ".csv":
342
+ df = pd.read_csv(path, dtype=str).fillna("")
343
+ return df.to_csv(index=False), meta
344
+ if ext == ".txt":
345
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
346
+ return f.read(), meta
347
+ raise ValueError("์ง€์› ํ™•์žฅ์ž: pdf/docx/csv/txt")
348
+
349
+ def chunk_text(t: str, chunk=800, overlap=200) -> List[str]:
350
+ t = re.sub(r"\s+", " ", (t or "")).strip()
351
+ if not t: return []
352
+ out=[]; s=0
353
+ while s < len(t):
354
+ e=min(len(t), s+chunk)
355
+ out.append(t[s:e])
356
+ if e==len(t): break
357
+ s=max(0, e-overlap)
358
+ return out
359
+
360
+ def build_index(files, chunk=800, overlap=200):
361
+ global _faiss, _chunks, _dim
362
+ _chunks=[]
363
+
364
+ all_texts=[]
365
+ for f in files or []:
366
+ txt, meta = extract_text(f.name)
367
+ cks = chunk_text(txt, chunk, overlap)
368
+ for ci, c in enumerate(cks):
369
+ _chunks.append({"id": len(_chunks), "text": c, "meta": {"source": meta["source"], "chunk_id": ci}})
370
+ all_texts.append(c)
371
+
372
+ if not all_texts:
373
+ return "โš ๏ธ ์ถ”์ถœ ํ…์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
374
+
375
+ vecs = embed_texts(all_texts)
376
+ _dim = vecs.shape[1]
377
+ _faiss = faiss.IndexFlatIP(_dim)
378
+ _faiss.add(vecs)
379
+ return f"โœ… ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์™„๋ฃŒ ยท ์ฒญํฌ {len(all_texts)}๊ฐœ ยท dim={_dim}"
380
+
381
+ def search_index(query: str, k=5) -> List[Dict]:
382
+ if _faiss is None or not _chunks: return []
383
+ qv = embed_texts([query])
384
+ scores, idxs = _faiss.search(qv, k)
385
+ res=[]
386
+ for rank, (i, s) in enumerate(zip(idxs[0].tolist(), scores[0].tolist()), 1):
387
+ if 0<=i<len(_chunks):
388
+ item = _chunks[i]
389
+ res.append({"rank": rank, "score": float(s),
390
+ "text": item["text"], "source": item["meta"]["source"], "chunk_id": item["meta"]["chunk_id"]})
391
+ return res
392
+
393
+ def make_context_block(query: str, k=5) -> Tuple[str, str]:
394
+ hits = search_index(query, k)
395
+ if not hits: return "(no RAG context)", "(no sources)"
396
+ ctx_lines=[]; srcs=[]
397
+ for h in hits:
398
+ ctx_lines.append(f"[{h['rank']}|{h['source']}|#{h['chunk_id']}|{h['score']:.3f}] {h['text']}")
399
+ srcs.append(f"{h['rank']}. {h['source']} (chunk {h['chunk_id']})")
400
+ return "\n\n".join(ctx_lines), "\n".join(srcs)
401
+
402
+ # -----------------------------
403
+ # ์ตœ์ข… ํ…œํ”Œ๋ฆฟ/ํ”„๋กฌํ”„ํŠธ
404
+ # -----------------------------
405
+ TEMPLATE = """# ๋ชฉ์ (Purpose)
406
+ - ์šฐ๋ฆฌ๋Š” [{domain}]์—์„œ [{out}]์„ ์‹ ์†ยท์ •ํ™•ยท์žฌ์‚ฌ์šฉ ๏ฟฝ๏ฟฝ๏ฟฝ๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ ์ž ํ•œ๋‹ค.
407
+ - ์ตœ์ข… ๋…์ž: [{user}] | ํ•ด๊ฒฐํ•  Pain: [{pain}]
408
+
409
+ # ์„ ํƒ ์ปจํ…์ŠคํŠธ(์ž์œ  ์ž…๋ ฅ ํฌํ•จ)
410
+ - ์„ธ๋ถ€ ๋„๋ฉ”์ธ: {subdomain}
411
+ - ๋„๋ฉ”์ธ ๋ฉ”๋ชจ: {domain_note}
412
+ - Pain ์„ธ๋ถ€/๋ฉ”๋ชจ: {pain_sub} | {pain_note}
413
+ - Output ์„ธ๋ถ€/์ŠคํŽ™: {out_sub} | {out_note}
414
+ - User ์„ธ๋ถ€/๋ฉ”๋ชจ: {user_sub} | {user_note}
415
+
416
+ # ์‚ฐ์ถœ๋ฌผ ์ •์˜(What to produce)
417
+ - ์‚ฐ์ถœ๋ฌผ ์ข…๋ฅ˜: [{out}]
418
+ - ์‚ฌ์šฉ ๋งฅ๋ฝ/๋ชฉํ‘œ KPI: [{kpi}]
419
+ - ์„ฑ๊ณต ๊ธฐ์ค€(ํ†ต๊ณผ ์กฐ๊ฑด):
420
+ 1) [{format_hint}]์„ 100% ์ค€์ˆ˜
421
+ 2) [{audience}]์—๊ฒŒ ๊ฐ€์น˜๊ฐ€ ์ฆ‰์‹œ ๋ณด์ž„
422
+ 3) ์ŠคํŒธ/๊ณผ์žฅ/์• ๋งค์–ด ์—†์Œ
423
+
424
+ # ํ˜•์‹/ํ†ค ๊ฐ€๋“œ๋ ˆ์ผ(Format & Tone)
425
+ - ํ˜•์‹: [{format_hint}]
426
+ - ํ†ค: [{tone}] | ๊ธˆ์ง€: [{ng}]
427
+
428
+ # ํ˜ผํ•ฉ ํ”„๋กฌํ”„ํŒ… ๊ธฐ์ˆ (Why these techniques)
429
+ {tech_blocks}
430
+
431
+ {rag_section}
432
+
433
+ # ์ž‘์„ฑ ์ž‘์—…(Tasks)
434
+ 1) **์ดˆ์•ˆ v1**: [{format_hint}]์— ๋งž์ถ˜ ๋ณธ๋ฌธ ์ž‘์„ฑ
435
+ 2) **๋Œ€์•ˆ ์ƒ์„ฑ**: ํ•ต์‹ฌ ๋ฌธ๊ตฌ/์ œ๋ชฉ/CTA ํ›„๋ณด N๊ฐœ ์ƒ์„ฑ
436
+ 3) **์ž์ฒด ๊ฒ€์ฆ**: ์ŠคํŒธ์–ด/๊ธˆ์น™์–ด/๊ธธ์ด/๊ฐœ์ธํ™” ๋ณ€์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ†ต๊ณผ
437
+ 4) **์š”์•ฝ v2**: 5์ค„ ์š”์•ฝ(๋ฌธ์ œโ†’๊ฐ€์น˜โ†’์ฆ๊ฑฐโ†’CTAโ†’๋‹ค์Œ ์•ก์…˜)
438
+
439
+ # ์ถœ๋ ฅ ํ˜•์‹(Output)
440
+ - ์„น์…˜๋ณ„๋กœ ๊ตฌ๋ถ„ํ•ด ๋งˆํฌ๋‹ค์šด์œผ๋กœ ์ถœ๋ ฅ(์ œ๋ชฉ, ์˜คํ”„๋‹, ๊ฐ€์น˜์ œ์•ˆ, ์ฆ๊ฑฐ, CTA, PS ๋“ฑ)
441
+ - ๋งˆ์ง€๋ง‰: **๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ**(โ–ก ๊ฐœ์ธํ™” ํ•„๋“œ โ–ก ๊ธˆ์น™์–ด ์—†์Œ โ–ก ๊ธธ์ด ์ค€์ˆ˜ โ–ก ๊ฐ€์น˜/์ฆ๊ฑฐ/CTA ๋ช…ํ™•)
442
+ - **๋‹ค์Œ ์•ก์…˜ 3๊ฐ€์ง€**(์˜ˆ: ์บ˜๋ฆฐ๋” ๋งํฌ, ์‚ฌ๋ก€ PDF, 2์ฐจ ์—ฐ๋ฝ ์Šค์ผ€์ค„)
443
+ """
444
+
445
+ def compose_final_prompt(
446
+ domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs, techs,
447
+ domain_note, pain_note, out_note, user_note, kpi_text, tone_text, ng_text, format_override,
448
+ fewshot_text, rag_text,
449
+ use_rag, rag_topk
450
+ ):
451
+ if not domain_key or not outs or not users or not techs:
452
+ return "โš ๏ธ โ€˜๊ตฌ๋ถ„/Output/User/๊ธฐ์ˆ โ€™์„ ์„ ํƒํ•˜์„ธ์š”."
453
+
454
+ format_hint = guess_format_hint(outs, format_override)
455
+ audience = ", ".join(user_subs or users)
456
+ kpi = kpi_text.strip() or ("์˜คํ”ˆ์œจ/CTR/์‘๋‹ตยท๋ฏธํŒ… ์ˆ˜" if "์™ธ๋ถ€" in domain_key else "์ •ํ™•์„ฑ/๊ทผ๊ฑฐ/๊ฐ€๋…์„ฑ")
457
+ tone = tone_text.strip() or ("์ง์ ‘์ ยท๊ฐ„๊ฒฐยทROI ์ค‘์‹ฌ" if "์„ธ์ผ์ฆˆ" in " ".join(users or []) else "๋ช…๋ฃŒยท๊ฐ๊ด€ยท๊ฐ„๊ฒฐ")
458
+ ng = ng_text.strip() or "๊ณผ์žฅยท๊ทผ๊ฑฐ ์—†๋Š” ์ˆ˜์น˜ยท๋ชจํ˜ธํ•œ ํ‘œํ˜„"
459
+
460
+ blocks=[]
461
+ for t in techs:
462
+ reasons = uniq(reason_from_pain(t, pains, pain_subs) +
463
+ reason_from_output(t, outs, out_subs) +
464
+ reason_from_user(t, users, user_subs))
465
+ head = f"- **{t}** โ€” {TECH_GLOSSARY.get(t,{}).get('desc','')}"
466
+ tail = ""
467
+ if reasons:
468
+ tail = "\n - ์„ ์ • ๊ทผ๊ฑฐ: " + " / ".join(reasons[:4])
469
+ dom = domain_reco(t, outs)
470
+ if dom: tail += f"\n - ๋„๋ฉ”์ธ ๊ถŒ์žฅ: {dom}"
471
+ blocks.append((head + tail).rstrip())
472
+
473
+ rag_section = ""
474
+ if use_rag:
475
+ query = f"{' '.join(outs)} | {' '.join(out_subs or [])} | {' '.join(users)} | {' '.join(pains)} | {pain_note} | {out_note}"
476
+ ctx, srcs = make_context_block(query, k=int(rag_topk))
477
+ rag_section = f"""# RAG ์ปจํ…์ŠคํŠธ
478
+ - ์งˆ์˜: {query}
479
+ - Top-{rag_topk} ์ปจํ…์ŠคํŠธ:
480
+ {ctx}
481
+
482
+ - ์ถœ์ฒ˜:
483
+ {srcs}
484
+
485
+ - ์ง€์‹œ: **์ปจํ…์ŠคํŠธ ๋ฒ”์œ„ ๋‚ด์—์„œ๋งŒ** ์„œ์ˆ ํ•˜๊ณ , ๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ โ€˜๊ทผ๊ฑฐ ์—†์Œโ€™์œผ๋กœ ๋ช…์‹œ."""
486
+ else:
487
+ if rag_text.strip():
488
+ rag_section = f"# ์ฐธ๊ณ  ์ž๋ฃŒ ๋ฉ”๋ชจ(์ˆ˜๊ธฐ)\n{rag_text.strip()}"
489
+
490
+ if fewshot_text.strip():
491
+ blocks.append(f"- **Few-shot ์˜ˆ์‹œ**: {fewshot_text.strip()}")
492
+
493
+ return TEMPLATE.format(
494
+ domain=domain_key,
495
+ out=", ".join(outs),
496
+ user=", ".join(users) + (" / " + ", ".join(user_subs) if user_subs else ""),
497
+ pain=", ".join(pains) + (" / " + ", ".join(pain_subs) if pain_subs else ""),
498
+ subdomain=", ".join(subdomains or ["-"]),
499
+ domain_note=domain_note or "-",
500
+ pain_sub=", ".join(pain_subs or ["-"]),
501
+ pain_note=pain_note or "-",
502
+ out_sub=", ".join(out_subs or ["-"]),
503
+ out_note=out_note or "-",
504
+ user_sub=", ".join(user_subs or ["-"]),
505
+ user_note=user_note or "-",
506
+ kpi=kpi,
507
+ audience=audience,
508
+ format_hint=format_hint,
509
+ tone=tone,
510
+ ng=ng,
511
+ tech_blocks="\n\n".join(blocks),
512
+ rag_section=rag_section or ""
513
+ )
514
+
515
+ # -----------------------------
516
+ # ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  ์„ค๋ช… ํ•œ๊บผ๋ฒˆ์— ๋ณด๊ธฐ
517
+ # -----------------------------
518
+ def render_multi_rationales(tech_list: List[str],
519
+ domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs):
520
+ if not tech_list:
521
+ return "๊ธฐ์ˆ ์„ ํ•˜๋‚˜ ์ด์ƒ ์„ ํƒํ•˜์„ธ์š”."
522
+ sections=[]
523
+ for t in tech_list:
524
+ sec = build_rationale_detailed(t, domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs)
525
+ if sec:
526
+ sections.append(sec)
527
+ return ("\n\n---\n\n".join(sections)).strip()
528
+
529
+ # -----------------------------
530
+ # UI
531
+ # -----------------------------
532
+ DEFAULT_DOMAIN = "2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜"
533
+ D = CATALOG[DEFAULT_DOMAIN]
534
+
535
+ with gr.Blocks(title="Mixed Prompt Composer โ€” Rationale + RAG + Multi Preview") as demo:
536
+ gr.Markdown("## ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… โ€” ์ฝค๋ณด + ์ž์œ  ์ž…๋ ฅ + **๊ฐ•ํ™” Rationale** + **RAG(FAISS)** + **์—ฌ๋Ÿฌ ๊ธฐ์ˆ  ํ•œ๊บผ๋ฒˆ์— ๋ฏธ๋ฆฌ๋ณด๊ธฐ**")
537
+
538
+ # 1) ๋„๋ฉ”์ธ/์„ธ๋ถ€ + ์ž์œ ์ž…๋ ฅ
539
+ with gr.Row():
540
+ domain = gr.Dropdown(label="๊ตฌ๋ถ„(๋Œ€๋ถ„๋ฅ˜)", choices=list(CATALOG.keys()), value=DEFAULT_DOMAIN)
541
+ subdomain = gr.Dropdown(label="์„ธ๋ถ€ ๋„๋ฉ”์ธ(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["subdomains"], multiselect=True, value=["์‹œ์žฅ๋ณด๊ณ ์„œ"])
542
+ domain_note = gr.Textbox(label="๋„๋ฉ”์ธ ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ๋ถ๋ฏธ SaaS B2B ์ค‘์‹ฌ, ์ตœ์‹  ๋ถ„๊ธฐ ๊ธฐ์ค€")
543
+
544
+ # 2) Pain + ์ž์œ ์ž…๋ ฅ
545
+ with gr.Row():
546
+ pains = gr.Dropdown(label="Pain Points(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["pains"], multiselect=True, value=["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ"])
547
+ pain_detail = gr.Dropdown(label="Pain ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=PAIN_SUB["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ"], multiselect=True, value=["์ž๋ฃŒ ์ˆ˜์ง‘"])
548
+ pain_note = gr.Textbox(label="Pain ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ์œ ๋ฃŒ ๋ฆฌํฌํŠธ ์ ‘๊ทผ ์ œํ•œ, 2024 Q3 ๋ฐ์ดํ„ฐ ํ•„์š”")
549
+
550
+ # 3) Output + ์ž์œ ์ž…๋ ฅ
551
+ with gr.Row():
552
+ outs = gr.Dropdown(label="Outputs(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["outputs"], multiselect=True, value=["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ"])
553
+ out_detail = gr.Dropdown(label="Output ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=OUTPUT_SUB["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ"], multiselect=True, value=["๋ฆฌ์„œ์น˜ ๋ธŒ๋ฆฌํ”„"])
554
+ out_note = gr.Textbox(label="Output ์ŠคํŽ™(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: 8~10p, ํ‘œ/๊ทธ๋ž˜ํ”„ 4๊ฐœ, ๊ฒฝ์Ÿ 5์‚ฌ, ์ถœ์ฒ˜ ๊ฐ์ฃผ ํ•„์ˆ˜")
555
+
556
+ # 4) User + ์ž์œ ์ž…๋ ฅ
557
+ with gr.Row():
558
+ users = gr.Dropdown(label="Users(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["users"], multiselect=True, value=["์ „๋žตํŒ€"])
559
+ user_detail = gr.Dropdown(label="User ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=USER_SUB["์ „๋žตํŒ€"], multiselect=True, value=["Corp Strategy"])
560
+ user_note = gr.Textbox(label="User ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ๊ฒฝ์˜์ง„ ๋ธŒ๋ฆฌํ•‘์šฉ 1-pager ์š”์•ฝ ์ถ”๊ฐ€ ํ•„์š”")
561
+
562
+ # 5) ์ž๋™ ์ถ”์ฒœ & ๊ธฐ์ˆ  ์„ ํƒ
563
+ with gr.Row():
564
+ auto_btn = gr.Button("๐Ÿ”ฎ Mixed Prompts ์ž๋™ ์ถ”์ฒœ")
565
+ techs = gr.Dropdown(label="Mixed Prompts(๋ณต์ˆ˜ ์„ ํƒ/์ˆ˜์ • ๊ฐ€๋Šฅ)", choices=ALL_TECHS, multiselect=True)
566
+
567
+ # 6) ๊ณ ๊ธ‰ ์„ค์ •
568
+ with gr.Accordion("๊ณ ๊ธ‰ ์„ค์ •(์˜ค๋ฒ„๋ผ์ด๋“œ & ์˜ˆ์‹œ/๊ทผ๊ฑฐ)", open=False):
569
+ with gr.Row():
570
+ kpi_text = gr.Textbox(label="KPI(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ์ธ์‚ฌ์ดํŠธ ์ •ํ™•์„ฑ, ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ์ง€์›")
571
+ tone_text = gr.Textbox(label="ํ†ค(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ๊ฐ๊ด€ยท๊ฐ„๊ฒฐยท๋ฐ์ดํ„ฐ ์ค‘์‹ฌ")
572
+ with gr.Row():
573
+ ng_text = gr.Textbox(label="๊ธˆ์ง€์–ด/NG(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ๊ณผ์žฅ, ์ถœ์ฒ˜ ๋ฏธํ‘œ๊ธฐ, ์ถ”์ • ์ˆ˜์น˜ ๋‹จ์ •ํ™”")
574
+ format_override = gr.Textbox(label="ํ˜•์‹(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„")
575
+ fewshot_text = gr.Textbox(label="Few-shot ์˜ˆ์‹œ(์ž์œ  ์ž…๋ ฅ)", lines=4, placeholder="[์ƒ˜ํ”Œ] ์ œ๋ชฉ/์˜คํ”„๋‹/๊ทผ๊ฑฐ/CTAโ€ฆ")
576
+ rag_text = gr.Textbox(label="์ฐธ๊ณ  ์ž๋ฃŒ ๋ฉ”๋ชจ(์ˆ˜๊ธฐ RAG ๋Œ€์ฒด/๋ณด์™„)", lines=3)
577
+
578
+ # 7) ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  โ€œํ•œ๊บผ๋ฒˆ์—โ€ ์ƒ์„ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
579
+ with gr.Accordion("๐Ÿ“– ์„ ํƒํ•œ ํ”„๋กฌํ”„ํŠธ ๊ธฐ์ˆ  โ€” ์„ค๋ช… & Rationale (๋ณต์ˆ˜ ํŽผ์ณ๋ณด๊ธฐ)", open=True):
580
+ with gr.Row():
581
+ tech_preview = gr.Dropdown(label="๋ฏธ๋ฆฌ๋ณผ ๊ธฐ์ˆ (๋ณต์ˆ˜ ์„ ํƒ)", choices=ALL_TECHS, multiselect=True)
582
+ from_selected_btn = gr.Button("ํ˜„์žฌ Mixed Prompts ์ „์ฒด ์„ค๋ช… ๋ณด๊ธฐ")
583
+ rationale_md = gr.Markdown("์—ฌ๋Ÿฌ ๊ธฐ์ˆ ์„ ์„ ํƒํ•˜๋ฉด **์„ค๋ช… & ์„ ์ • ๊ทผ๊ฑฐ**๋ฅผ ํ•œ๊บผ๋ฒˆ์— ํŽผ์นฉ๋‹ˆ๋‹ค.")
584
+
585
+ # 8) RAG ์—…๋กœ๋“œ/์ธ๋ฑ์‹ฑ
586
+ gr.Markdown("### ๐Ÿ“š RAG โ€” ํŒŒ์ผ ์—…๋กœ๋“œ โ†’ ์ธ๋ฑ์‹ฑ โ†’ ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… ์ž๋™ ์ฃผ์ž…")
587
+ with gr.Row():
588
+ rag_files = gr.Files(label="๋ฌธ์„œ ์—…๋กœ๋“œ(pdf/docx/csv/txt ๋ณต์ˆ˜)", file_count="multiple", file_types=[".pdf",".docx",".csv",".txt"])
589
+ build_btn = gr.Button("๐Ÿ”จ ์ธ๋ฑ์Šค ๊ตฌ์ถ•")
590
+ rag_status = gr.Markdown("์ƒํƒœ: ์ธ๋ฑ์Šค ์—†์Œ")
591
+ with gr.Row():
592
+ use_rag = gr.Checkbox(label="RAG ์‚ฌ์šฉ", value=False)
593
+ rag_topk = gr.Slider(1,10,value=5,step=1,label="RAG Top-K")
594
+
595
+ # 9) ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ
596
+ gen_btn = gr.Button("๐Ÿš€ ๊ตฌ์กฐํ™”๋œ ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… ์ƒ์„ฑ")
597
+ final_box = gr.Textbox(label="์ตœ์ข… ์œตํ•ฉ ํ”„๋กฌํ”„ํŠธ (๋ณต์‚ฌํ•˜์—ฌ Gemini/Claude/Perplexity/OpenAI์— ์‚ฌ์šฉ)", lines=28, show_copy_button=True)
598
+
599
+ # ===== ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ =====
600
+ def on_domain_change(dkey):
601
+ cfg = CATALOG.get(dkey, {})
602
+ return (gr.update(choices=cfg.get("subdomains", []), value=[]),
603
+ gr.update(choices=cfg.get("pains", []), value=[]),
604
+ gr.update(choices=[], value=[]),
605
+ gr.update(choices=cfg.get("outputs", []), value=[]),
606
+ gr.update(choices=[], value=[]),
607
+ gr.update(choices=cfg.get("users", []), value=[]),
608
+ gr.update(choices=[], value=[]))
609
+ domain.change(on_domain_change, inputs=[domain],
610
+ outputs=[subdomain, pains, pain_detail, outs, out_detail, users, user_detail])
611
+
612
+ def on_pain_change(ps):
613
+ ch=[]; [ch.extend(PAIN_SUB.get(p, [])) for p in (ps or [])]
614
+ return gr.update(choices=uniq(ch), value=[])
615
+ pains.change(on_pain_change, inputs=[pains], outputs=[pain_detail])
616
+
617
+ def on_out_change(osel):
618
+ ch=[]; [ch.extend(OUTPUT_SUB.get(o, [])) for o in (osel or [])]
619
+ return gr.update(choices=uniq(ch), value=[])
620
+ outs.change(on_out_change, inputs=[outs], outputs=[out_detail])
621
+
622
+ def on_user_change(usel):
623
+ ch=[]; [ch.extend(USER_SUB.get(u, [])) for u in (usel or [])]
624
+ return gr.update(choices=uniq(ch), value=[])
625
+ users.change(on_user_change, inputs=[users], outputs=[user_detail])
626
+
627
+ def do_auto(dkey, ps, osel, usel):
628
+ rec = auto_recommend(dkey, ps or [], osel or [], usel or [])
629
+ return gr.update(value=rec, choices=uniq(ALL_TECHS + rec))
630
+ auto_btn.click(do_auto, inputs=[domain, pains, outs, users], outputs=[techs])
631
+
632
+ # ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  โ€œํ•œ๊บผ๋ฒˆ์—โ€ ์ƒ์„ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
633
+ tech_preview.change(
634
+ render_multi_rationales,
635
+ inputs=[tech_preview, domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail],
636
+ outputs=[rationale_md]
637
+ )
638
+ from_selected_btn.click(
639
+ lambda cur: gr.update(value=cur),
640
+ inputs=[techs],
641
+ outputs=[tech_preview]
642
+ ).then(
643
+ render_multi_rationales,
644
+ inputs=[tech_preview, domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail],
645
+ outputs=[rationale_md]
646
+ )
647
+
648
+ # RAG ์ธ๋ฑ์‹ฑ
649
+ build_btn.click(lambda files: build_index(files, 800, 200),
650
+ inputs=[rag_files], outputs=[rag_status])
651
+
652
+ # ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
653
+ gen_btn.click(
654
+ compose_final_prompt,
655
+ inputs=[
656
+ domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail, techs,
657
+ # free text
658
+ domain_note, pain_note, out_note, user_note,
659
+ # overrides
660
+ kpi_text, tone_text, ng_text, format_override,
661
+ # extras
662
+ fewshot_text, rag_text,
663
+ # RAG
664
+ use_rag, rag_topk
665
+ ],
666
+ outputs=[final_box]
667
+ )
668
+
669
+ # ================== ๋Ÿฐ์น˜ (Spaces/๋กœ์ปฌ ๊ณตํ†ต) ==================
670
+ if __name__ == "__main__":
671
+ # (์„ ํƒ) Basic Auth: Space Secrets์— HF_AUTH_LIST="alice:pw1,bob:pw2"
672
+ auth_pairs = []
673
+ if os.getenv("HF_AUTH_LIST", "").strip():
674
+ for pair in os.getenv("HF_AUTH_LIST").split(","):
675
+ if ":" in pair:
676
+ u, p = pair.split(":", 1)
677
+ auth_pairs.append((u.strip(), p.strip()))
678
+
679
+ launch_kwargs = {"server_name": "0.0.0.0"}
680
+ if auth_pairs:
681
+ launch_kwargs["auth"] = auth_pairs
682
+
683
+ demo.queue() # Gradio Queue ํ™œ์„ฑํ™”(๋™์‹œ ์ ‘์† ์•ˆ์ „)
684
+ demo.launch(**launch_kwargs)