jeongkee's picture
Create app.py
e4c8587 verified
# -*- coding: utf-8 -*-
# Mixed Prompt Composer โ€” Combo + Free-text + Strong Rationale + RAG(FAISS)
# Gradio 4.x / Hugging Face Spaces ํ˜ธํ™˜
import os, re, json
from typing import List, Dict, Tuple
import gradio as gr
# ===== RAG deps =====
import faiss
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from pypdf import PdfReader
from docx import Document as Docx
# =============== ๋ชจ๋ธ ์›Œ๋ฐ์—…(์ตœ์ดˆ ๋กœ๋”ฉ ์ง€์—ฐ ์™„ํ™”) ===============
EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
try:
_WARMUP = SentenceTransformer(EMBED_MODEL_NAME)
except Exception:
_WARMUP = None
# -----------------------------
# 0) ๋ฐ์ดํ„ฐ์…‹/๊ธ€๋กœ์„œ๋ฆฌ
# -----------------------------
ALL_TECHS = [
"Persona Prompting","Few-shot Prompting","Self-consistency Prompting","Output Formatting",
"Chain-of-Thought (CoT)","Constrained Prompting","RAG Prompting","Step-back Prompting","Role Prompting",
]
TECH_GLOSSARY = {
"Persona Prompting": {
"desc": "๋Œ€์ƒ ์—ญํ• /์„ธ๊ทธ๋จผํŠธ์˜ ์–ธ์–ดยทKPIยท๊ด€์‹ฌ์‚ฌ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ •๋ ฌํ•ด ๋ฐ˜์‘๋ฅ ยท๊ณต๊ฐ๋„๋ฅผ ๋†’์ž„.",
"purpose": "๋ˆ„๊ตฌ์—๊ฒŒ ๋งํ•˜๋Š”์ง€ ๋ถ„๋ช…ํžˆ ํ•ด ๊ฐ€์น˜๊ฐ€ โ€˜๊ทธ๋“ค์˜ ์–ธ์–ดโ€™๋กœ ์ „๋‹ฌ๋˜๊ฒŒ ํ•จ.",
"mechanics": [
"์—ญํ• /๊ถŒํ•œ/๊ด€์‹ฌ KPI ๋ช…์‹œ(์˜ˆ: Sales Leader=ํŒŒ์ดํ”„๋ผ์ธยท์Šน๋ฅ ยท๋ฆฌ๋“œํƒ€์ž„).",
"ํ†ค/๊ธˆ์น™์–ด/์„ ํ˜ธ ํ‘œํ˜„ ์ •์˜(์ง์„คยท๊ฐ„๊ฒฐยท์ˆซ์ž/ROI, ๋ชจํ˜ธ์–ด ๊ธˆ์ง€).",
"ํŽ˜๋ฅด์†Œ๋‚˜๋ณ„ ๋ฌธ์žฅ ๋งคํ•‘(๋ฌธ์ œโ†’๊ฐ€์น˜โ†’์ฆ๊ฑฐโ†’CTA)."
],
"example": "์˜ˆ) โ€œ์ด๋ฒˆ ๋ถ„๊ธฐ ํŒŒ์ดํ”„๋ผ์ธ 18% ๋ณด๊ฐ• ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ๋น ๋ฅธ ์•ก์…˜ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค.โ€"
},
"Few-shot Prompting": {
"desc": "์†Œ์ˆ˜์˜ ๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ๋ฅผ ์ œ๊ณตํ•ด ํ†คยท๊ตฌ์กฐยท๋ฆฌ๋“ฌ์„ ๋ชจ์‚ฌ, ํ’ˆ์งˆ ํŽธ์ฐจโ†“ยท์†๋„โ†‘.",
"purpose": "๊ฒ€์ฆ๋œ ํŒจํ„ด ๋ณต์ œ๋กœ ์ผ๊ด€์„ฑ ํ™•๋ณด.",
"mechanics": [
"์ƒ˜ํ”Œ 2~3๊ฐœ๋ฅผ ํ—ค๋“œ๋ผ์ธ/์˜คํ”„๋‹/๊ทผ๊ฑฐ/CTA ํŒจํ„ด์œผ๋กœ ์ œ๊ณต.",
"โ€˜์ด ํ†ค์„ ๋ชจ์‚ฌํ•ด 3๊ฐœ ๋ณ€ํ˜•โ€™์ฒ˜๋Ÿผ ๋‹ค๋ณ€๋Ÿ‰ ํ›„๋ณด ์ƒ์„ฑ."
],
"example": "[์ƒ˜ํ”Œ] {์‚ฐ์—…} ํŒ€๋“ค์˜ {์„ฑ๊ณผ}(์‚ฌ๋ก€). ์ด๋ฒˆ์ฃผ {์ˆ˜/๋ชฉ} {11:00/16:00} 15๋ถ„ ํ†ตํ™” ๊ฐ€๋Šฅํ•˜์‹ค๊นŒ์š”?"
},
"Self-consistency Prompting": {
"desc": "์—ฌ๋Ÿฌ ํ›„๋ณด์•ˆ ์ƒ์„ฑ ํ›„ ์ž์ฒด ์ฑ„์ (์ฒดํฌ๋ฆฌ์ŠคํŠธ/์Šค์ฝ”์–ด๋ง)์œผ๋กœ ์ตœ์ ์•ˆ ์„ ํƒ.",
"purpose": "A/B ํ…Œ์ŠคํŠธ ๋‚ด์žฅ์œผ๋กœ ํ’ˆ์งˆ ํ–ฅ์ƒ.",
"mechanics": [
"์ œ๋ชฉ5ยท๋ฐ”๋””3ยทCTA3 ๋“ฑ ๋‹ค๋ณ€๋Ÿ‰ ์ƒ์„ฑ.",
"์ฒดํฌ๋ฆฌ์ŠคํŠธ(๊ฐœ์ธํ™”/๋ช…๋ฃŒ/๊ฐ€์น˜/์ŠคํŒธํšŒํ”ผ/CTA) ์ฑ„์ โ†’Top-1 ์ถœ๋ ฅ."
],
"example": "์ถœ๋ ฅ: ํ›„๋ณด ๋ฆฌ์ŠคํŠธ + ์ฑ„์ ํ‘œ + ์ตœ์ข… ์ถ”์ฒœ 1~2์•ˆ."
},
"Output Formatting": {
"desc": "์‚ฐ์ถœ๋ฌผ์˜ ํ˜•์‹/์„น์…˜/ํ•„๋“œ ๊ณ ์ •(ํ‘œ/์„น์…˜/JSON ๋“ฑ)์œผ๋กœ ์†๋„ยท๊ฐ€๋…์„ฑยท์žฌ์‚ฌ์šฉ์„ฑ ํ™•๋ณด.",
"purpose": "์Šน์ธ ๋ฃจํ”„ ๋‹จ์ถ•, ํ‘œ์ค€ํ™”.",
"mechanics": [
"์œ ํ˜•๋ณ„ ํ…œํ”Œ๋ฆฟ ๊ฐ•์ œ(์ด๋ฉ”์ผ/์นดํ”ผ/๋ณด๊ณ ์„œ/PRD/API).",
"ํ•„์ˆ˜ยท์„ ํƒ ํ•„๋“œ/๊ธธ์ด/๊ธˆ์น™์–ด ๋ช…์‹œ."
],
"example": "์ด๋ฉ”์ผ: ์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS"
},
"Chain-of-Thought (CoT)": {
"desc": "๋ชฉํ‘œโ†’์ง€ํ‘œโ†’๋Œ€์•ˆโ†’์„ ํƒโ†’์‹คํ–‰์˜ ๋‹จ๊ณ„์  ์ถ”๋ก ์œผ๋กœ ๋…ผ๋ฆฌ ๋น„์•ฝ ์ตœ์†Œํ™”.",
"purpose": "๋ถ„์„/์ „๋žต ๋ฌธ์„œ์˜ ๋…ผ๋ฆฌ ์ผ๊ด€์„ฑ.",
"mechanics": ["๊ฐ ๋‹จ๊ณ„์— ๊ฐ€์ •/๊ทผ๊ฑฐ/๋Œ€์•ˆ/๋ฆฌ์Šคํฌ/๊ถŒ๊ณ  ์ฒดํฌ ์งˆ๋ฌธ ๋ถ€์—ฌ."],
"example": "์š”์•ฝโ†’ํ˜„ํ™ฉโ†’๊ณผ์ œโ†’๋ถ„์„โ†’์ธ์‚ฌ์ดํŠธโ†’๊ถŒ๊ณ โ†’ํ•œ๊ณ„"
},
"Constrained Prompting": {
"desc": "๊ธธ์ดยท๊ธˆ์น™์–ดยทํ•„์ˆ˜ํ•„๋“œยทJSON ์Šคํ‚ค๋งˆ ๋“ฑ ์ œ์•ฝ ์ค€์ˆ˜.",
"purpose": "๋ธŒ๋žœ๋“œ/๋ฒ•๋ฌด/์‹ฌ์˜ ๋ฆฌ์Šคํฌ ์ตœ์†Œํ™”.",
"mechanics": ["์ œ๋ชฉ โ‰ค 30์ž, ๊ธˆ์น™์–ด ์ œ๊ฑฐ, ํ•„์ˆ˜ ํ‚ค ๋ˆ„๋ฝ ์‹œ ์žฌ์ƒ์„ฑ."],
"example": "JSON ์Šคํ‚ค๋งˆ ์ค€์ˆ˜ ์ถœ๋ ฅ / ํ‘œ์ค€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํฌํ•จ"
},
"RAG Prompting": {
"desc": "์™ธ๋ถ€ ๋ณด๊ณ ์„œ/DB/๋ฌธ์„œ์˜ ๊ทผ๊ฑฐ ์ฃผ์ž…์œผ๋กœ ์ตœ์‹ ์„ฑยท์‹ ๋ขฐ์„ฑ ํ™•๋ณด.",
"purpose": "์ถ”์ •/ํ™˜๊ฐ ๋ฐฉ์ง€, ์ถœ์ฒ˜ ๊ธฐ๋ฐ˜ ์„œ์ˆ .",
"mechanics": ["โ€˜๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ ์ถ”์ • ๊ธˆ์ง€, ์ถœ์ฒ˜ ๋ฉ”๋ชจโ€™ ์ง€์‹œ.", "์ธ์šฉ/๊ฐ์ฃผ/๋งํฌ ํ‘œ๊ธฐ."],
"example": "์˜ˆ) Gartner MQ 2024, ๊ณต์‹œ 2024Q3, Crunchbase 2024.07"
},
"Step-back Prompting": {
"desc": "์ƒ์œ„ ๋ชฉ์ /์›์น™์—์„œ ์ถœ๋ฐœํ•ด ์˜๋ฏธ ์ค‘์‹ฌ์œผ๋กœ ์žฌํ•ด์„.",
"purpose": "โ€˜์™œ ์ค‘์š”ํ•œ๊ฐ€โ€™์— ๋จผ์ € ๋‹ตํ•ด ๊ฒฝ์˜ ์‹œ์‚ฌ์  ๊ฐ•ํ™”.",
"mechanics": ["์ƒ์œ„ ๋ชฉํ‘œโ†’ํ•ต์‹ฌ ์›์น™โ†’ํ˜„์žฌ ์„ ํƒ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ."],
"example": "โ€˜๋น„์šฉ 20%โ†“โ€™๊ฐ€ ์ „๋žต KPI์— ์–ด๋–ป๊ฒŒ ๊ธฐ์—ฌํ•˜๋Š”์ง€ ์—ฐ๊ฒฐ"
},
"Role Prompting": {
"desc": "โ€˜๋‹น์‹ ์€ PM/์ „๋žต๊ฐ€/UX ๋ผ์ดํ„ฐโ€ฆโ€™์ฒ˜๋Ÿผ ๊ด€์  ๊ณ ์ •์œผ๋กœ ๋ชฉ์  ์ ํ•ฉ์„ฑโ†‘.",
"purpose": "์ง๋ฌด๋ณ„ ์–ธ์–ด/ํŒ๋‹จ ๊ธฐ์ค€ ์ผ์น˜.",
"mechanics": ["์—ญํ• ยทKPIยท๊ฒฐ์ •๊ถŒยท๋ฆฌ์Šคํฌ ๊ด€์  ๋ช…์‹œ."],
"example": "PM: ๋ฌธ์ œ์ •์˜/AC/๋ฆฌ์Šคํฌ | ์ „๋žต: ์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /๊ฑฐ๋ฒ„๋„Œ์Šค"
},
}
CATALOG = {
"1 ์™ธ๋ถ€ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜": {
"subdomains": ["์ด๋ฉ”์ผ(์ฝœ๋“œ/์›œ)", "๊ด‘๊ณ /๋žœ๋”ฉ", "PR/๋ณด๋„์ž๋ฃŒ", "SNS/์˜์ƒ"],
"pains": ["๋ฐ˜์‘๋ฅ  ์ €์กฐ", "๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜", "์ž‘์„ฑ ์‹œ๊ฐ„ ๊ณผ๋‹ค", "A/B ํ…Œ์ŠคํŠธ ๋ถ€๋‹ด"],
"outputs": ["์ฝœ๋“œ๋ฉ”์ผ", "๊ด‘๊ณ  ์นดํ”ผ", "๋ณด๋„์ž๋ฃŒ", "๋žœ๋”ฉํŽ˜์ด์ง€ ์„น์…˜"],
"users": ["์„ธ์ผ์ฆˆ", "๋งˆ์ผ€ํŒ…ํŒ€", "PRํŒ€", "์ฐฝ์—…์ž"]
},
"2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜": {
"subdomains": ["์‹œ์žฅ๋ณด๊ณ ์„œ", "๊ฒฝ์Ÿ๋ถ„์„", "VOC ๋ถ„์„", "ํŽ˜๋ฅด์†Œ๋‚˜"],
"pains": ["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ", "๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™•", "์ถœ์ฒ˜/๊ธฐ๊ฐ„ ๋ˆ„๋ฝ", "๋ฆฌ์„œ์น˜ ์ž‘์„ฑ ๋ถ€๋‹ด"],
"outputs": ["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ", "๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„", "ํŽ˜๋ฅด์†Œ๋‚˜", "VOC ์š”์•ฝ"],
"users": ["์ „๋žตํŒ€", "๊ธฐํšํŒ€", "์ปจ์„คํ„ดํŠธ", "IR/ํˆฌ์ž์ค€๋น„ํŒ€"]
},
"3 ์ œํ’ˆยทUX ๋ฌธ์„œ": {
"subdomains": ["PRD/์š”๊ตฌ์‚ฌํ•ญ", "์œ ์Šค์ผ€์ด์Šค", "UX ๋งˆ์ดํฌ๋กœ์นดํ”ผ", "๋ฆด๋ฆฌ์Šค ๋…ธํŠธ"],
"pains": ["์š”๊ตฌ์‚ฌํ•ญ ์–ธ์–ดํ™” ๋‚œํ•ญ", "UX ์นดํ”ผ ๋ถˆ์ผ์น˜", "์˜์‚ฌ๊ฒฐ์ • ๊ธฐ์ค€ ๋ถˆ๋ช…ํ™•"],
"outputs": ["PRD", "์œ ์Šค์ผ€์ด์Šค", "UX ์นดํ”ผ", "๋ฆด๋ฆฌ์Šค ๋…ธํŠธ"],
"users": ["PM", "UX ๋””์ž์ด๋„ˆ", "๊ฐœ๋ฐœํŒ€", "QA"]
},
}
PAIN_SUB = {
"๋ฐ˜์‘๋ฅ  ์ €์กฐ": ["์ œ๋ชฉ/ํ”„๋ฆฌํ—ค๋”", "ํƒ€๊ฒŒํŒ…", "CTA ์•ฝํ•จ", "์‹ ๋ขฐ ๊ทผ๊ฑฐ ๋ถ€์กฑ"],
"๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜": ["ํ†ค/๋ณด์ด์Šค", "ํฌ๋งท/๊ธธ์ด", "์ฑ„๋„/ํŽ˜์ด์‹ฑ"],
"์ž‘์„ฑ ์‹œ๊ฐ„ ๊ณผ๋‹ค": ["ํ…œํ”Œ๋ฆฟ ๋ถ€์žฌ", "์˜ˆ์‹œ ๋ถ€์กฑ", "์Šน์ธ ๋ฃจํ”„ ์ง€์—ฐ"],
"๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ": ["์ž๋ฃŒ ์ˆ˜์ง‘", "์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ", "์ตœ์‹ ์„ฑ"],
"๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™•": ["์„ธ๊ทธ๋จผํŠธ ์ •์˜", "JTBD ๋ชจํ˜ธ", "ํŽ˜์ธ/๊ฒŒ์ธ"],
}
OUTPUT_SUB = {
"์ฝœ๋“œ๋ฉ”์ผ": ["์‹ ๊ทœ ์ธ๋ฐ”์šด๋“œ", "์ฝœ๋“œ ์•„์›ƒ๋ฐ”์šด๋“œ", "ํ›„์†/๋ฆฌ๋งˆ์ธ๋“œ", "์ดํƒˆ ์žฌ์ฐธ์—ฌ"],
"๊ด‘๊ณ  ์นดํ”ผ": ["์›น ๋ฐฐ๋„ˆ", "๊ฒ€์ƒ‰๊ด‘๊ณ ", "SNS ์นด๋“œ", "์•ฑ ํ‘ธ์‹œ"],
"์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ": ["ํƒ‘๋‹ค์šด(ํƒ์ƒ‰)", "๋ฐ”ํ…€์—… ์ถ”์ •", "ํ˜ผํ•ฉ ์ ‘๊ทผ", "๋ฆฌ์„œ์น˜ ๋ธŒ๋ฆฌํ”„"],
"๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„": ["๊ธฐ๋Šฅ ๋น„๊ตํ‘œ", "๊ฐ€๊ฒฉ/ํŒจํ‚ค์ง€", "ํฌ์ง€์…”๋‹ ๋งต", "SWOT"],
"PRD": ["๋ฌธ์ œ์ •์˜", "๋ชฉํ‘œ/์ง€ํ‘œ", "๋ฒ”์œ„/๋น„๋ฒ”์œ„", "์ˆ˜์šฉ๊ธฐ์ค€(AC)", "๋ฆฌ์Šคํฌ"],
}
USER_SUB = {
"์„ธ์ผ์ฆˆ": ["BDR", "AE", "AM", "Sales Leader"],
"๋งˆ์ผ€ํŒ…ํŒ€": ["ํผํฌ๋จผ์Šค", "์ฝ˜ํ…์ธ ", "๋ธŒ๋žœ๋“œ", "๊ทธ๋กœ์Šค"],
"PRํŒ€": ["์ฝ”ํผ๋ ˆ์ดํŠธ", "ํ”„๋กœ๋•ํŠธ PR"],
"์ฐฝ์—…์ž": ["Seed", "Series A+", "๋ถ€ํŠธ์ŠคํŠธ๋žฉ"],
"์ „๋žตํŒ€": ["Corp Strategy", "Biz Ops"],
"PM": ["Jr PM", "Sr PM", "Group PM"],
"UX ๋””์ž์ด๋„ˆ": ["UX Writer", "Product Designer"],
"๊ฐœ๋ฐœํŒ€": ["FE", "BE", "ML", "Infra"],
"QA": ["QA ์—”์ง€๋‹ˆ์–ด", "QA ๋ฆฌ๋“œ"]
}
# -----------------------------
# ์œ ํ‹ธ
# -----------------------------
def uniq(xs: List[str]) -> List[str]:
if not xs: return []
s=set(); out=[]
for x in xs:
if x not in s:
s.add(x); out.append(x)
return out
AUTO_RULES_PAIN = {
"๋ฐ˜์‘๋ฅ ": ["Persona Prompting", "Few-shot Prompting", "Self-consistency Prompting"],
"๋ถˆ์ผ์น˜": ["Output Formatting", "Constrained Prompting"],
"์ž‘์„ฑ": ["Output Formatting", "Few-shot Prompting"],
"๊ฒฝ์Ÿ": ["RAG Prompting", "Chain-of-Thought (CoT)"],
"์š”๊ตฌ": ["Persona Prompting", "Step-back Prompting"],
}
AUTO_RULES_OUTPUT = {
"์ด๋ฉ”์ผ": ["Persona Prompting", "Output Formatting", "Few-shot Prompting", "Self-consistency Prompting"],
"์นดํ”ผ": ["Few-shot Prompting", "Self-consistency Prompting", "Output Formatting"],
"๋ณด๊ณ ์„œ": ["RAG Prompting", "Chain-of-Thought (CoT)", "Output Formatting"],
"PRD": ["Role Prompting", "Constrained Prompting", "Chain-of-Thought (CoT)"],
}
AUTO_RULES_USER = {
"์„ธ์ผ์ฆˆ": ["Persona Prompting", "Few-shot Prompting", "Self-consistency Prompting"],
"๋งˆ์ผ€ํŒ…": ["Few-shot Prompting", "Self-consistency Prompting"],
"์ „๋žต": ["Chain-of-Thought (CoT)", "Step-back Prompting", "RAG Prompting"],
"PM": ["Role Prompting", "Constrained Prompting"],
"๋ฐ์ดํ„ฐ": ["RAG Prompting", "Constrained Prompting"],
}
def auto_recommend(domain_key, pains, outs, users):
rec=[]
if domain_key=="1 ์™ธ๋ถ€ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜":
rec+=["Persona Prompting","Few-shot Prompting","Self-consistency Prompting","Output Formatting"]
if domain_key=="2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜":
rec+=["RAG Prompting","Chain-of-Thought (CoT)","Output Formatting","Step-back Prompting"]
if domain_key=="3 ์ œํ’ˆยทUX ๋ฌธ์„œ":
rec+=["Role Prompting","Constrained Prompting","Chain-of-Thought (CoT)","Output Formatting"]
for p in pains or []:
for k,ts in AUTO_RULES_PAIN.items():
if k in p: rec+=ts
for o in outs or []:
for k,ts in AUTO_RULES_OUTPUT.items():
if k in o: rec+=ts
for u in users or []:
for k,ts in AUTO_RULES_USER.items():
if k in u: rec+=ts
rec.append("Output Formatting")
return uniq(rec)
def guess_format_hint(outs: List[str], override: str="") -> str:
if override.strip():
return override.strip()
j=" ".join(outs or [])
if any(k in j for k in ["PRD","API","ADR","์ •์ฑ…","SOP","์œ ์Šค์ผ€์ด์Šค","FAQ"]): return "JSON/ํ‘œ/๋ถˆ๋ฆฟ(ํ•„๋“œ ํ‚ค ๊ณ ์ •)"
if any(k in j for k in ["๋ณด๊ณ ์„œ","๋ธŒ๋ฆฌํ”„","์š”์•ฝ","๋ฌธ์„œ","1-Pager"]): return "์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„"
if any(k in j for k in ["์นดํ”ผ","๊ด‘๊ณ ","๋žœ๋”ฉ"]): return "ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒํ—ค๋“œ/๋ฐ”๋””/CTA"
if any(k in j for k in ["์ด๋ฉ”์ผ","๋ฉ”์ผ","์ฝœ๋“œ๋ฉ”์ผ"]): return "์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS"
return "๋ชฉ์ฐจ/์š”์•ฝ/๋ณธ๋ฌธ/๊ถŒ๊ณ /CTA"
# -----------------------------
# Rationale(๊ฐ•ํ™”ํŒ)
# -----------------------------
def reason_from_pain(tech: str, pains: List[str], pain_subs: List[str]) -> List[str]:
R=[]
jp=" ".join(pains or []) + " " + " ".join(pain_subs or [])
if "๋ฐ˜์‘๋ฅ " in jp:
if tech=="Persona Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ์„ธ๊ทธ๋จผํŠธ ๋งž์ถค ์–ดํœ˜/์–ด์กฐ๋กœ ์ฒด๊ฐ ๊ฐ€์น˜ ์ƒ์Šน")
if tech=="Few-shot Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ ํŒจํ„ด ๋ณต์ œ")
if tech=="Self-consistency Prompting": R.append("๋ฐ˜์‘๋ฅ  ์ €์กฐ โ†’ ๋‹ค๋ณ€๋Ÿ‰ ํ›„๋ณด ์ƒ์„ฑโ†’์ž์ฒด ์ฑ„์ ์œผ๋กœ Top-1")
if "๋ถˆ์ผ์น˜" in jp or "ํ†ค/๋ณด์ด์Šค" in jp or "ํฌ๋งท" in jp:
if tech=="Output Formatting": R.append("๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜ โ†’ ํ˜•์‹/์„น์…˜ ๊ณ ์ •์œผ๋กœ ์ •ํ•ฉ์„ฑโ†‘")
if tech=="Constrained Prompting": R.append("๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜ โ†’ ๊ธˆ์น™/๊ธธ์ด/ํ•„์ˆ˜ํ•„๋“œ ๊ฐ•์ œ")
if "์ž‘์„ฑ ์‹œ๊ฐ„" in jp or "์˜ˆ์‹œ ๋ถ€์กฑ" in jp:
if tech in {"Few-shot Prompting","Output Formatting"}: R.append("์ž‘์„ฑ์‹œ๊ฐ„ ๊ณผ๋‹ค/์˜ˆ์‹œ ๋ถ€์กฑ โ†’ ํ…œํ”Œ๋ฆฟ + ์˜ˆ์‹œ ๊ธฐ๋ฐ˜ ์†๋„โ†‘")
if "๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด" in jp:
if tech=="RAG Prompting": R.append("๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ โ†’ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ๊ทผ๊ฑฐ ์ฃผ์ž…(์ตœ์‹ ์„ฑ/์‹ ๋ขฐ์„ฑ)")
if tech=="Chain-of-Thought (CoT)": R.append("๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„ ๊ตฌ์กฐํ™”(CoT)๋กœ ์ธ์‚ฌ์ดํŠธ ๋ช…๋ฃŒํ™”")
if "์š”๊ตฌ ๋ถˆ๋ช…ํ™•" in jp:
if tech in {"Persona Prompting","Step-back Prompting"}: R.append("๊ณ ๊ฐ ์š”๊ตฌ ๋ถˆ๋ช…ํ™• โ†’ ์ƒ์œ„ ๋ชฉ์ /JTBD ์ •๋ ฌ")
return R
def reason_from_output(tech: str, outs: List[str], out_subs: List[str]) -> List[str]:
R=[]
jo=" ".join(outs or []) + " " + " ".join(out_subs or [])
if "๋ฉ”์ผ" in jo or "์นดํ”ผ" in jo or "๋žœ๋”ฉ" in jo:
if tech=="Output Formatting": R.append("์นดํ”ผ/๋ฉ”์ผ โ†’ ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒ/๋ฐ”๋””/CTA ๊ณ ์ •์ด ์„ฑ๊ณผ ์ขŒ์šฐ")
if tech=="Self-consistency Prompting": R.append("๋ฉ”์‹œ์ง€ ํ›„๋ณด ๋‹ค๋ณ€๋Ÿ‰ ์ƒ์„ฑโ†’์ตœ์ ์•ˆ ์„ ํƒ ํ•„์š”")
if tech=="Few-shot Prompting": R.append("์ฑ„๋„๋ณ„ ํ†ค/๊ธธ์ด ์ฐจ์ด๋ฅผ ์˜ˆ์‹œ๋กœ ๋น ๋ฅด๊ฒŒ ์ ์‘")
if "๋ณด๊ณ ์„œ" in jo or "๋ถ„์„" in jo:
if tech=="RAG Prompting": R.append("๋ณด๊ณ ์„œ/๋ถ„์„ โ†’ ์ˆ˜์น˜ยท์ถœ์ฒ˜ ์ตœ์‹ ์„ฑ ๋ณด์žฅ")
if tech=="Chain-of-Thought (CoT)": R.append("๋ณด๊ณ ์„œ/๋ถ„์„ โ†’ ๋‹จ๊ณ„์  ๊ตฌ์กฐ(CoT)๋กœ ๋…ผ๋ฆฌ ๊ฐ•ํ™”")
if "PRD" in jo or "API" in jo:
if tech in {"Constrained Prompting","Role Prompting"}: R.append("PRD/API โ†’ JSON/ํ•„๋“œ ๊ฐ•์ œ + ์ง๋ฌด ๊ด€์  ๊ณ ์ • ํ•„์š”")
return R
def reason_from_user(tech: str, users: List[str], user_subs: List[str]) -> List[str]:
R=[]
ju=" ".join(users or []) + " " + " ".join(user_subs or [])
if "์„ธ์ผ์ฆˆ" in ju:
if tech=="Persona Prompting": R.append("Sales๋Š” ํŒŒ์ดํ”„๋ผ์ธ/์Šน๋ฅ  ์–ธ์–ด ์„ ํ˜ธ โ†’ ํŽ˜๋ฅด์†Œ๋‚˜ ํ†ค ์ ์šฉ")
if tech=="Few-shot Prompting": R.append("์˜์—… ๋ ˆํผ๋Ÿฐ์Šค ์‚ฌ๋ก€ ๋ชจ์‚ฌ๊ฐ€ ์„ค๋“๋ ฅโ†‘")
if "์ „๋žต" in ju:
if tech in {"Chain-of-Thought (CoT)","Step-back Prompting","RAG Prompting"}:
R.append("์ „๋žต์กฐ์ง์€ ๊ทผ๊ฑฐ/๋…ผ๋ฆฌ/์‹œ์‚ฌ์  ์ค‘์‹œ โ†’ CoT+RAG+Step-back ์ ํ•ฉ")
if "PM" in ju or "UX" in ju:
if tech in {"Role Prompting","Constrained Prompting"}:
R.append("PM/UX๋Š” ํ•„๋“œ/AC/ํ†ค ํ‘œ์ค€ ํ•„์š” โ†’ ์—ญํ•  ๊ณ ์ • + ์ œ์•ฝ ๊ฐ•์ œ")
return R
def domain_reco(tech: str, outs: List[str]) -> str:
j=" ".join(outs or [])
if tech=="Output Formatting":
if "๋ฉ”์ผ" in j: return "์ด๋ฉ”์ผ: ์ œ๋ชฉ/์˜คํ”„๋‹/๊ฐ€์น˜์ œ์•ˆ/์ฆ๊ฑฐ/CTA/PS ํ˜•์‹ ๊ณ ์ •"
if "์นดํ”ผ" in j or "๋žœ๋”ฉ" in j: return "์นดํ”ผ/๋žœ๋”ฉ: ํ—ค๋“œ๋ผ์ธ/์„œ๋ธŒ/๋ฐ”๋””/CTA ๋ธ”๋ก ๊ณ ์ •"
if "๋ณด๊ณ ์„œ" in j: return "๋ณด๊ณ ์„œ: ์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„ ์„น์…˜ ๊ณ ์ •"
if "PRD" in j: return "PRD: ํ•„์ˆ˜ ํ•„๋“œ(JSON/ํ‘œ) ๊ฐ•์ œ"
return "์‚ฐ์ถœ๋ฌผ ํ‘œ์ค€ ์„น์…˜ ๊ณ ์ •"
if tech=="Persona Prompting": return "์„ธ๊ทธ๋จผํŠธยท์—ญํ•  KPI ์–ธ์–ด ์ •๋ ฌ"
if tech=="Few-shot Prompting": return "๊ณ ์„ฑ๊ณผ ์˜ˆ์‹œ ๊ตฌ์กฐ/ํ†ค ๋ชจ์‚ฌ"
if tech=="Self-consistency Prompting": return "๋‹ค์ค‘ ํ›„๋ณด ์ƒ์„ฑโ†’์ž์ฒด ์ฑ„์ โ†’Top-1"
if tech=="Constrained Prompting": return "๊ธธ์ด/๊ธˆ์น™์–ด/ํ•„์ˆ˜ํ•„๋“œ ๊ฐ•์ œ"
if tech=="RAG Prompting": return "์™ธ๋ถ€ ๋ฆฌํฌํŠธ/๋ฐฑ์„œ ๊ทผ๊ฑฐ ์ฃผ์ž…"
if tech=="Chain-of-Thought (CoT)": return "๋ชฉํ‘œโ†’์ง€ํ‘œโ†’๋Œ€์•ˆโ†’๊ถŒ๊ณ  ๋‹จ๊ณ„ํ™”"
if tech=="Step-back Prompting": return "์ƒ์œ„ ๋ชฉ์ /์›์น™์—์„œ ์˜๋ฏธ ์žฌํ•ด์„"
if tech=="Role Prompting": return "์—ญํ•  ๊ณ ์ •์œผ๋กœ ๊ด€์  ์ผ์น˜"
return "-"
def build_rationale_detailed(
tech: str, domain_key: str, subdomains: List[str],
pains: List[str], pain_subs: List[str],
outs: List[str], out_subs: List[str],
users: List[str], user_subs: List[str]
) -> str:
if not tech: return ""
g = TECH_GLOSSARY.get(tech, {})
desc, purpose, mechs, example = g.get("desc",""), g.get("purpose",""), g.get("mechanics",[]), g.get("example","")
R = []
R += reason_from_pain(tech, pains, pain_subs)
R += reason_from_output(tech, outs, out_subs)
R += reason_from_user(tech, users, user_subs)
R = uniq(R)
parts = [
f"## {tech}",
f"**์„ค๋ช…**: {desc}",
f"- **์ ์šฉ ๋ชฉ์ **: {purpose}" if purpose else "",
"- **์ž‘๋™ ๋ฐฉ์‹**:\n - " + "\n - ".join(mechs) if mechs else "",
f"- **๋„๋ฉ”์ธ ๊ถŒ์žฅ**: {domain_reco(tech, outs)}",
f"- **์„ ์ • ๊ทผ๊ฑฐ(์„ ํƒ ๋ฐ˜์˜)**:\n - " + "\n - ".join(R) if R else "- **์„ ์ • ๊ทผ๊ฑฐ**: (์„ ํƒ ํ•ญ๋ชฉ์— ๋”ฐ๋ผ ์ž๋™ ์ƒ์„ฑ)",
f"- **์˜ˆ์‹œ**: {example}"
]
return "\n".join([p for p in parts if p.strip()])
# -----------------------------
# RAG: ์—…๋กœ๋“œโ†’์ฒญํ‚นโ†’์ž„๋ฒ ๋”ฉโ†’FAISSโ†’๊ฒ€์ƒ‰
# -----------------------------
_model = None
_faiss = None
_chunks = [] # [{id, text, meta}]
_dim = 384
def get_model():
global _model
if _model is None:
_model = SentenceTransformer(EMBED_MODEL_NAME)
return _model
def embed_texts(texts: List[str]) -> np.ndarray:
model = get_model()
vecs = model.encode(texts, normalize_embeddings=True)
return np.array(vecs, dtype="float32")
def extract_text(path: str) -> Tuple[str, Dict]:
name = os.path.basename(path)
ext = os.path.splitext(path)[1].lower()
meta = {"source": name}
if ext == ".pdf":
reader = PdfReader(path)
pages = []
for i, p in enumerate(reader.pages):
try: pages.append(p.extract_text() or "")
except: pages.append("")
return "\n".join(pages), meta
if ext == ".docx":
d = Docx(path)
return "\n".join(p.text for p in d.paragraphs), meta
if ext == ".csv":
df = pd.read_csv(path, dtype=str).fillna("")
return df.to_csv(index=False), meta
if ext == ".txt":
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read(), meta
raise ValueError("์ง€์› ํ™•์žฅ์ž: pdf/docx/csv/txt")
def chunk_text(t: str, chunk=800, overlap=200) -> List[str]:
t = re.sub(r"\s+", " ", (t or "")).strip()
if not t: return []
out=[]; s=0
while s < len(t):
e=min(len(t), s+chunk)
out.append(t[s:e])
if e==len(t): break
s=max(0, e-overlap)
return out
def build_index(files, chunk=800, overlap=200):
global _faiss, _chunks, _dim
_chunks=[]
all_texts=[]
for f in files or []:
txt, meta = extract_text(f.name)
cks = chunk_text(txt, chunk, overlap)
for ci, c in enumerate(cks):
_chunks.append({"id": len(_chunks), "text": c, "meta": {"source": meta["source"], "chunk_id": ci}})
all_texts.append(c)
if not all_texts:
return "โš ๏ธ ์ถ”์ถœ ํ…์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
vecs = embed_texts(all_texts)
_dim = vecs.shape[1]
_faiss = faiss.IndexFlatIP(_dim)
_faiss.add(vecs)
return f"โœ… ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์™„๋ฃŒ ยท ์ฒญํฌ {len(all_texts)}๊ฐœ ยท dim={_dim}"
def search_index(query: str, k=5) -> List[Dict]:
if _faiss is None or not _chunks: return []
qv = embed_texts([query])
scores, idxs = _faiss.search(qv, k)
res=[]
for rank, (i, s) in enumerate(zip(idxs[0].tolist(), scores[0].tolist()), 1):
if 0<=i<len(_chunks):
item = _chunks[i]
res.append({"rank": rank, "score": float(s),
"text": item["text"], "source": item["meta"]["source"], "chunk_id": item["meta"]["chunk_id"]})
return res
def make_context_block(query: str, k=5) -> Tuple[str, str]:
hits = search_index(query, k)
if not hits: return "(no RAG context)", "(no sources)"
ctx_lines=[]; srcs=[]
for h in hits:
ctx_lines.append(f"[{h['rank']}|{h['source']}|#{h['chunk_id']}|{h['score']:.3f}] {h['text']}")
srcs.append(f"{h['rank']}. {h['source']} (chunk {h['chunk_id']})")
return "\n\n".join(ctx_lines), "\n".join(srcs)
# -----------------------------
# ์ตœ์ข… ํ…œํ”Œ๋ฆฟ/ํ”„๋กฌํ”„ํŠธ
# -----------------------------
TEMPLATE = """# ๋ชฉ์ (Purpose)
- ์šฐ๋ฆฌ๋Š” [{domain}]์—์„œ [{out}]์„ ์‹ ์†ยท์ •ํ™•ยท์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ ์ž ํ•œ๋‹ค.
- ์ตœ์ข… ๋…์ž: [{user}] | ํ•ด๊ฒฐํ•  Pain: [{pain}]
# ์„ ํƒ ์ปจํ…์ŠคํŠธ(์ž์œ  ์ž…๋ ฅ ํฌํ•จ)
- ์„ธ๋ถ€ ๋„๋ฉ”์ธ: {subdomain}
- ๋„๋ฉ”์ธ ๋ฉ”๋ชจ: {domain_note}
- Pain ์„ธ๋ถ€/๋ฉ”๋ชจ: {pain_sub} | {pain_note}
- Output ์„ธ๋ถ€/์ŠคํŽ™: {out_sub} | {out_note}
- User ์„ธ๋ถ€/๋ฉ”๋ชจ: {user_sub} | {user_note}
# ์‚ฐ์ถœ๋ฌผ ์ •์˜(What to produce)
- ์‚ฐ์ถœ๋ฌผ ์ข…๋ฅ˜: [{out}]
- ์‚ฌ์šฉ ๋งฅ๋ฝ/๋ชฉํ‘œ KPI: [{kpi}]
- ์„ฑ๊ณต ๊ธฐ์ค€(ํ†ต๊ณผ ์กฐ๊ฑด):
1) [{format_hint}]์„ 100% ์ค€์ˆ˜
2) [{audience}]์—๊ฒŒ ๊ฐ€์น˜๊ฐ€ ์ฆ‰์‹œ ๋ณด์ž„
3) ์ŠคํŒธ/๊ณผ์žฅ/์• ๋งค์–ด ์—†์Œ
# ํ˜•์‹/ํ†ค ๊ฐ€๋“œ๋ ˆ์ผ(Format & Tone)
- ํ˜•์‹: [{format_hint}]
- ํ†ค: [{tone}] | ๊ธˆ์ง€: [{ng}]
# ํ˜ผํ•ฉ ํ”„๋กฌํ”„ํŒ… ๊ธฐ์ˆ (Why these techniques)
{tech_blocks}
{rag_section}
# ์ž‘์„ฑ ์ž‘์—…(Tasks)
1) **์ดˆ์•ˆ v1**: [{format_hint}]์— ๋งž์ถ˜ ๋ณธ๋ฌธ ์ž‘์„ฑ
2) **๋Œ€์•ˆ ์ƒ์„ฑ**: ํ•ต์‹ฌ ๋ฌธ๊ตฌ/์ œ๋ชฉ/CTA ํ›„๋ณด N๊ฐœ ์ƒ์„ฑ
3) **์ž์ฒด ๊ฒ€์ฆ**: ์ŠคํŒธ์–ด/๊ธˆ์น™์–ด/๊ธธ์ด/๊ฐœ์ธํ™” ๋ณ€์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ†ต๊ณผ
4) **์š”์•ฝ v2**: 5์ค„ ์š”์•ฝ(๋ฌธ์ œโ†’๊ฐ€์น˜โ†’์ฆ๊ฑฐโ†’CTAโ†’๋‹ค์Œ ์•ก์…˜)
# ์ถœ๋ ฅ ํ˜•์‹(Output)
- ์„น์…˜๋ณ„๋กœ ๊ตฌ๋ถ„ํ•ด ๋งˆํฌ๋‹ค์šด์œผ๋กœ ์ถœ๋ ฅ(์ œ๋ชฉ, ์˜คํ”„๋‹, ๊ฐ€์น˜์ œ์•ˆ, ์ฆ๊ฑฐ, CTA, PS ๋“ฑ)
- ๋งˆ์ง€๋ง‰: **๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ**(โ–ก ๊ฐœ์ธํ™” ํ•„๋“œ โ–ก ๊ธˆ์น™์–ด ์—†์Œ โ–ก ๊ธธ์ด ์ค€์ˆ˜ โ–ก ๊ฐ€์น˜/์ฆ๊ฑฐ/CTA ๋ช…ํ™•)
- **๋‹ค์Œ ์•ก์…˜ 3๊ฐ€์ง€**(์˜ˆ: ์บ˜๋ฆฐ๋” ๋งํฌ, ์‚ฌ๋ก€ PDF, 2์ฐจ ์—ฐ๋ฝ ์Šค์ผ€์ค„)
"""
def compose_final_prompt(
domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs, techs,
domain_note, pain_note, out_note, user_note, kpi_text, tone_text, ng_text, format_override,
fewshot_text, rag_text,
use_rag, rag_topk
):
if not domain_key or not outs or not users or not techs:
return "โš ๏ธ โ€˜๊ตฌ๋ถ„/Output/User/๊ธฐ์ˆ โ€™์„ ์„ ํƒํ•˜์„ธ์š”."
format_hint = guess_format_hint(outs, format_override)
audience = ", ".join(user_subs or users)
kpi = kpi_text.strip() or ("์˜คํ”ˆ์œจ/CTR/์‘๋‹ตยท๋ฏธํŒ… ์ˆ˜" if "์™ธ๋ถ€" in domain_key else "์ •ํ™•์„ฑ/๊ทผ๊ฑฐ/๊ฐ€๋…์„ฑ")
tone = tone_text.strip() or ("์ง์ ‘์ ยท๊ฐ„๊ฒฐยทROI ์ค‘์‹ฌ" if "์„ธ์ผ์ฆˆ" in " ".join(users or []) else "๋ช…๋ฃŒยท๊ฐ๊ด€ยท๊ฐ„๊ฒฐ")
ng = ng_text.strip() or "๊ณผ์žฅยท๊ทผ๊ฑฐ ์—†๋Š” ์ˆ˜์น˜ยท๋ชจํ˜ธํ•œ ํ‘œํ˜„"
blocks=[]
for t in techs:
reasons = uniq(reason_from_pain(t, pains, pain_subs) +
reason_from_output(t, outs, out_subs) +
reason_from_user(t, users, user_subs))
head = f"- **{t}** โ€” {TECH_GLOSSARY.get(t,{}).get('desc','')}"
tail = ""
if reasons:
tail = "\n - ์„ ์ • ๊ทผ๊ฑฐ: " + " / ".join(reasons[:4])
dom = domain_reco(t, outs)
if dom: tail += f"\n - ๋„๋ฉ”์ธ ๊ถŒ์žฅ: {dom}"
blocks.append((head + tail).rstrip())
rag_section = ""
if use_rag:
query = f"{' '.join(outs)} | {' '.join(out_subs or [])} | {' '.join(users)} | {' '.join(pains)} | {pain_note} | {out_note}"
ctx, srcs = make_context_block(query, k=int(rag_topk))
rag_section = f"""# RAG ์ปจํ…์ŠคํŠธ
- ์งˆ์˜: {query}
- Top-{rag_topk} ์ปจํ…์ŠคํŠธ:
{ctx}
- ์ถœ์ฒ˜:
{srcs}
- ์ง€์‹œ: **์ปจํ…์ŠคํŠธ ๋ฒ”์œ„ ๋‚ด์—์„œ๋งŒ** ์„œ์ˆ ํ•˜๊ณ , ๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ โ€˜๊ทผ๊ฑฐ ์—†์Œโ€™์œผ๋กœ ๋ช…์‹œ."""
else:
if rag_text.strip():
rag_section = f"# ์ฐธ๊ณ  ์ž๋ฃŒ ๋ฉ”๋ชจ(์ˆ˜๊ธฐ)\n{rag_text.strip()}"
if fewshot_text.strip():
blocks.append(f"- **Few-shot ์˜ˆ์‹œ**: {fewshot_text.strip()}")
return TEMPLATE.format(
domain=domain_key,
out=", ".join(outs),
user=", ".join(users) + (" / " + ", ".join(user_subs) if user_subs else ""),
pain=", ".join(pains) + (" / " + ", ".join(pain_subs) if pain_subs else ""),
subdomain=", ".join(subdomains or ["-"]),
domain_note=domain_note or "-",
pain_sub=", ".join(pain_subs or ["-"]),
pain_note=pain_note or "-",
out_sub=", ".join(out_subs or ["-"]),
out_note=out_note or "-",
user_sub=", ".join(user_subs or ["-"]),
user_note=user_note or "-",
kpi=kpi,
audience=audience,
format_hint=format_hint,
tone=tone,
ng=ng,
tech_blocks="\n\n".join(blocks),
rag_section=rag_section or ""
)
# -----------------------------
# ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  ์„ค๋ช… ํ•œ๊บผ๋ฒˆ์— ๋ณด๊ธฐ
# -----------------------------
def render_multi_rationales(tech_list: List[str],
domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs):
if not tech_list:
return "๊ธฐ์ˆ ์„ ํ•˜๋‚˜ ์ด์ƒ ์„ ํƒํ•˜์„ธ์š”."
sections=[]
for t in tech_list:
sec = build_rationale_detailed(t, domain_key, subdomains, pains, pain_subs, outs, out_subs, users, user_subs)
if sec:
sections.append(sec)
return ("\n\n---\n\n".join(sections)).strip()
# -----------------------------
# UI
# -----------------------------
DEFAULT_DOMAIN = "2 ์‹œ์žฅยท๊ณ ๊ฐ ๋ฆฌ์„œ์น˜"
D = CATALOG[DEFAULT_DOMAIN]
with gr.Blocks(title="Mixed Prompt Composer โ€” Rationale + RAG + Multi Preview") as demo:
gr.Markdown("## ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… โ€” ์ฝค๋ณด + ์ž์œ  ์ž…๋ ฅ + **๊ฐ•ํ™” Rationale** + **RAG(FAISS)** + **์—ฌ๋Ÿฌ ๊ธฐ์ˆ  ํ•œ๊บผ๋ฒˆ์— ๋ฏธ๋ฆฌ๋ณด๊ธฐ**")
# 1) ๋„๋ฉ”์ธ/์„ธ๋ถ€ + ์ž์œ ์ž…๋ ฅ
with gr.Row():
domain = gr.Dropdown(label="๊ตฌ๋ถ„(๋Œ€๋ถ„๋ฅ˜)", choices=list(CATALOG.keys()), value=DEFAULT_DOMAIN)
subdomain = gr.Dropdown(label="์„ธ๋ถ€ ๋„๋ฉ”์ธ(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["subdomains"], multiselect=True, value=["์‹œ์žฅ๋ณด๊ณ ์„œ"])
domain_note = gr.Textbox(label="๋„๋ฉ”์ธ ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ๋ถ๋ฏธ SaaS B2B ์ค‘์‹ฌ, ์ตœ์‹  ๋ถ„๊ธฐ ๊ธฐ์ค€")
# 2) Pain + ์ž์œ ์ž…๋ ฅ
with gr.Row():
pains = gr.Dropdown(label="Pain Points(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["pains"], multiselect=True, value=["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ"])
pain_detail = gr.Dropdown(label="Pain ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=PAIN_SUB["๊ฒฝ์Ÿ์‚ฌ ์ •๋ณด ๋ถ€์กฑ"], multiselect=True, value=["์ž๋ฃŒ ์ˆ˜์ง‘"])
pain_note = gr.Textbox(label="Pain ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ์œ ๋ฃŒ ๋ฆฌํฌํŠธ ์ ‘๊ทผ ์ œํ•œ, 2024 Q3 ๋ฐ์ดํ„ฐ ํ•„์š”")
# 3) Output + ์ž์œ ์ž…๋ ฅ
with gr.Row():
outs = gr.Dropdown(label="Outputs(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["outputs"], multiselect=True, value=["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ"])
out_detail = gr.Dropdown(label="Output ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=OUTPUT_SUB["์‹œ์žฅ์กฐ์‚ฌ ๋ณด๊ณ ์„œ"], multiselect=True, value=["๋ฆฌ์„œ์น˜ ๋ธŒ๋ฆฌํ”„"])
out_note = gr.Textbox(label="Output ์ŠคํŽ™(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: 8~10p, ํ‘œ/๊ทธ๋ž˜ํ”„ 4๊ฐœ, ๊ฒฝ์Ÿ 5์‚ฌ, ์ถœ์ฒ˜ ๊ฐ์ฃผ ํ•„์ˆ˜")
# 4) User + ์ž์œ ์ž…๋ ฅ
with gr.Row():
users = gr.Dropdown(label="Users(๋ณต์ˆ˜ ์„ ํƒ)", choices=D["users"], multiselect=True, value=["์ „๋žตํŒ€"])
user_detail = gr.Dropdown(label="User ์„ธ๋ถ€(๋ณต์ˆ˜ ์„ ํƒ)", choices=USER_SUB["์ „๋žตํŒ€"], multiselect=True, value=["Corp Strategy"])
user_note = gr.Textbox(label="User ๋ฉ”๋ชจ(์ž์œ  ์ž…๋ ฅ)", placeholder="์˜ˆ: ๊ฒฝ์˜์ง„ ๋ธŒ๋ฆฌํ•‘์šฉ 1-pager ์š”์•ฝ ์ถ”๊ฐ€ ํ•„์š”")
# 5) ์ž๋™ ์ถ”์ฒœ & ๊ธฐ์ˆ  ์„ ํƒ
with gr.Row():
auto_btn = gr.Button("๐Ÿ”ฎ Mixed Prompts ์ž๋™ ์ถ”์ฒœ")
techs = gr.Dropdown(label="Mixed Prompts(๋ณต์ˆ˜ ์„ ํƒ/์ˆ˜์ • ๊ฐ€๋Šฅ)", choices=ALL_TECHS, multiselect=True)
# 6) ๊ณ ๊ธ‰ ์„ค์ •
with gr.Accordion("๊ณ ๊ธ‰ ์„ค์ •(์˜ค๋ฒ„๋ผ์ด๋“œ & ์˜ˆ์‹œ/๊ทผ๊ฑฐ)", open=False):
with gr.Row():
kpi_text = gr.Textbox(label="KPI(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ์ธ์‚ฌ์ดํŠธ ์ •ํ™•์„ฑ, ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ์ง€์›")
tone_text = gr.Textbox(label="ํ†ค(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ๊ฐ๊ด€ยท๊ฐ„๊ฒฐยท๋ฐ์ดํ„ฐ ์ค‘์‹ฌ")
with gr.Row():
ng_text = gr.Textbox(label="๊ธˆ์ง€์–ด/NG(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ๊ณผ์žฅ, ์ถœ์ฒ˜ ๋ฏธํ‘œ๊ธฐ, ์ถ”์ • ์ˆ˜์น˜ ๋‹จ์ •ํ™”")
format_override = gr.Textbox(label="ํ˜•์‹(์˜ค๋ฒ„๋ผ์ด๋“œ)", placeholder="์˜ˆ: ์š”์•ฝ/ํ˜„ํ™ฉ/๊ฒฝ์Ÿ/์ธ์‚ฌ์ดํŠธ/๊ถŒ๊ณ /ํ•œ๊ณ„")
fewshot_text = gr.Textbox(label="Few-shot ์˜ˆ์‹œ(์ž์œ  ์ž…๋ ฅ)", lines=4, placeholder="[์ƒ˜ํ”Œ] ์ œ๋ชฉ/์˜คํ”„๋‹/๊ทผ๊ฑฐ/CTAโ€ฆ")
rag_text = gr.Textbox(label="์ฐธ๊ณ  ์ž๋ฃŒ ๋ฉ”๋ชจ(์ˆ˜๊ธฐ RAG ๋Œ€์ฒด/๋ณด์™„)", lines=3)
# 7) ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  โ€œํ•œ๊บผ๋ฒˆ์—โ€ ์ƒ์„ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
with gr.Accordion("๐Ÿ“– ์„ ํƒํ•œ ํ”„๋กฌํ”„ํŠธ ๊ธฐ์ˆ  โ€” ์„ค๋ช… & Rationale (๋ณต์ˆ˜ ํŽผ์ณ๋ณด๊ธฐ)", open=True):
with gr.Row():
tech_preview = gr.Dropdown(label="๋ฏธ๋ฆฌ๋ณผ ๊ธฐ์ˆ (๋ณต์ˆ˜ ์„ ํƒ)", choices=ALL_TECHS, multiselect=True)
from_selected_btn = gr.Button("ํ˜„์žฌ Mixed Prompts ์ „์ฒด ์„ค๋ช… ๋ณด๊ธฐ")
rationale_md = gr.Markdown("์—ฌ๋Ÿฌ ๊ธฐ์ˆ ์„ ์„ ํƒํ•˜๋ฉด **์„ค๋ช… & ์„ ์ • ๊ทผ๊ฑฐ**๋ฅผ ํ•œ๊บผ๋ฒˆ์— ํŽผ์นฉ๋‹ˆ๋‹ค.")
# 8) RAG ์—…๋กœ๋“œ/์ธ๋ฑ์‹ฑ
gr.Markdown("### ๐Ÿ“š RAG โ€” ํŒŒ์ผ ์—…๋กœ๋“œ โ†’ ์ธ๋ฑ์‹ฑ โ†’ ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… ์ž๋™ ์ฃผ์ž…")
with gr.Row():
rag_files = gr.Files(label="๋ฌธ์„œ ์—…๋กœ๋“œ(pdf/docx/csv/txt ๋ณต์ˆ˜)", file_count="multiple", file_types=[".pdf",".docx",".csv",".txt"])
build_btn = gr.Button("๐Ÿ”จ ์ธ๋ฑ์Šค ๊ตฌ์ถ•")
rag_status = gr.Markdown("์ƒํƒœ: ์ธ๋ฑ์Šค ์—†์Œ")
with gr.Row():
use_rag = gr.Checkbox(label="RAG ์‚ฌ์šฉ", value=False)
rag_topk = gr.Slider(1,10,value=5,step=1,label="RAG Top-K")
# 9) ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ
gen_btn = gr.Button("๐Ÿš€ ๊ตฌ์กฐํ™”๋œ ์œตํ•ฉ ํ”„๋กฌํ”„ํŒ… ์ƒ์„ฑ")
final_box = gr.Textbox(label="์ตœ์ข… ์œตํ•ฉ ํ”„๋กฌํ”„ํŠธ (๋ณต์‚ฌํ•˜์—ฌ Gemini/Claude/Perplexity/OpenAI์— ์‚ฌ์šฉ)", lines=28, show_copy_button=True)
# ===== ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ =====
def on_domain_change(dkey):
cfg = CATALOG.get(dkey, {})
return (gr.update(choices=cfg.get("subdomains", []), value=[]),
gr.update(choices=cfg.get("pains", []), value=[]),
gr.update(choices=[], value=[]),
gr.update(choices=cfg.get("outputs", []), value=[]),
gr.update(choices=[], value=[]),
gr.update(choices=cfg.get("users", []), value=[]),
gr.update(choices=[], value=[]))
domain.change(on_domain_change, inputs=[domain],
outputs=[subdomain, pains, pain_detail, outs, out_detail, users, user_detail])
def on_pain_change(ps):
ch=[]; [ch.extend(PAIN_SUB.get(p, [])) for p in (ps or [])]
return gr.update(choices=uniq(ch), value=[])
pains.change(on_pain_change, inputs=[pains], outputs=[pain_detail])
def on_out_change(osel):
ch=[]; [ch.extend(OUTPUT_SUB.get(o, [])) for o in (osel or [])]
return gr.update(choices=uniq(ch), value=[])
outs.change(on_out_change, inputs=[outs], outputs=[out_detail])
def on_user_change(usel):
ch=[]; [ch.extend(USER_SUB.get(u, [])) for u in (usel or [])]
return gr.update(choices=uniq(ch), value=[])
users.change(on_user_change, inputs=[users], outputs=[user_detail])
def do_auto(dkey, ps, osel, usel):
rec = auto_recommend(dkey, ps or [], osel or [], usel or [])
return gr.update(value=rec, choices=uniq(ALL_TECHS + rec))
auto_btn.click(do_auto, inputs=[domain, pains, outs, users], outputs=[techs])
# ์—ฌ๋Ÿฌ ๊ธฐ์ˆ  โ€œํ•œ๊บผ๋ฒˆ์—โ€ ์ƒ์„ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
tech_preview.change(
render_multi_rationales,
inputs=[tech_preview, domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail],
outputs=[rationale_md]
)
from_selected_btn.click(
lambda cur: gr.update(value=cur),
inputs=[techs],
outputs=[tech_preview]
).then(
render_multi_rationales,
inputs=[tech_preview, domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail],
outputs=[rationale_md]
)
# RAG ์ธ๋ฑ์‹ฑ
build_btn.click(lambda files: build_index(files, 800, 200),
inputs=[rag_files], outputs=[rag_status])
# ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
gen_btn.click(
compose_final_prompt,
inputs=[
domain, subdomain, pains, pain_detail, outs, out_detail, users, user_detail, techs,
# free text
domain_note, pain_note, out_note, user_note,
# overrides
kpi_text, tone_text, ng_text, format_override,
# extras
fewshot_text, rag_text,
# RAG
use_rag, rag_topk
],
outputs=[final_box]
)
# ================== ๋Ÿฐ์น˜ (Spaces/๋กœ์ปฌ ๊ณตํ†ต) ==================
if __name__ == "__main__":
# (์„ ํƒ) Basic Auth: Space Secrets์— HF_AUTH_LIST="alice:pw1,bob:pw2"
auth_pairs = []
if os.getenv("HF_AUTH_LIST", "").strip():
for pair in os.getenv("HF_AUTH_LIST").split(","):
if ":" in pair:
u, p = pair.split(":", 1)
auth_pairs.append((u.strip(), p.strip()))
launch_kwargs = {"server_name": "0.0.0.0"}
if auth_pairs:
launch_kwargs["auth"] = auth_pairs
demo.queue() # Gradio Queue ํ™œ์„ฑํ™”(๋™์‹œ ์ ‘์† ์•ˆ์ „)
demo.launch(**launch_kwargs)