# -*- 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 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)