human_interview / app.py
sujeongim
edit : gr
01dfce8
import gradio as gr
from huggingface_hub import InferenceClient
from litellm import completion
from dotenv import load_dotenv
import json
import re
load_dotenv()
# -------------------------------
# 1) ๊ณ ์ • 10๊ฐœ ์งˆ๋ฌธ ์ •์˜
# -------------------------------
FIXED_QUESTIONS = [
"Q1) ๋ณธ์ธ์˜ ์ „๊ณต/์—…๋ฌด ๋ถ„์•ผ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?",
"Q2) ์ตœ๊ทผ ๊ฐ€์žฅ ์ง‘์ค‘ํ•œ ํ”„๋กœ์ ํŠธ๋Š” ๋ฌด์—‡์ด์—ˆ๋‚˜์š”?",
"Q3) ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ ๊ฐ€์žฅ ์–ด๋ ค์› ๋˜ ์ ์€ ๋ฌด์—‡์ด์—ˆ๋‚˜์š”?",
"Q4) ์ฆ๊ฒจ ์“ฐ๋Š” ๊ฐœ๋ฐœ ์Šคํƒ(์–ธ์–ด/ํ”„๋ ˆ์ž„์›Œํฌ/๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)์„ ์•Œ๋ ค์ฃผ์„ธ์š”.",
"Q5) ํ˜‘์—… ์‹œ ๊ฐ€์žฅ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•˜๋Š” ์›์น™์€ ๋ฌด์—‡์ธ๊ฐ€์š”?",
"Q6) ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์œ„ํ•ด ๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ธ๊ฐ€์š”?",
"Q7) ํ…Œ์ŠคํŠธ/๊ฒ€์ฆ์„ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜๋‚˜์š”?",
"Q8) ๋ฐ์ดํ„ฐ/๋ฆฌ์†Œ์Šค๊ฐ€ ์ œํ•œ๋  ๋•Œ ์–ด๋–ค ์ „๋žต์„ ์“ฐ์‹œ๋‚˜์š”?",
"Q9) ์ตœ๊ทผ ๋ฐฐ์šด ๊ฒƒ ์ค‘ ๊ฐ€์žฅ ์œ ์šฉํ–ˆ๋˜ ๋‚ด์šฉ์€ ๋ฌด์—‡์ด์—ˆ๋‚˜์š”?",
"Q10) ์•ž์œผ๋กœ ๋‹ค๋ค„๋ณด๊ณ  ์‹ถ์€ ์ฃผ์ œ๋‚˜ ๊ธฐ์ˆ ์ด ์žˆ๋‚˜์š”?",
]
# -------------------------------
# 2) ๊ผฌ๋ฆฌ์งˆ๋ฌธ 20๊ฐœ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ
# (๊ณ ์ • 10๋ฌธ๋‹ต์„ ๋ฐ”ํƒ•์œผ๋กœ ์ถ”์ถœ)
# -------------------------------
FOLLOWUP_SYSTEM = (
"You are an excellent interviewer. Based on the given 10 Q/A pairs, "
"generate 20 SHORT, concrete, non-overlapping follow-up questions that deeply probe the user's answers. "
"Each question should be standalone and specific. Output as a numbered list 1..20."
)
def build_followup_user_prompt(qa_pairs):
"""
qa_pairs: list[tuple(question, answer)]
"""
lines = ["Below are 10 Q/A pairs. Generate 20 short follow-up questions.\n"]
for i, (q, a) in enumerate(qa_pairs, 1):
lines.append(f"[Q{i}] {q}")
lines.append(f"[A{i}] {a}\n")
lines.append("Return only the 20 questions as a numbered list (1..20).")
return "\n".join(lines)
def parse_numbered_list_to_lines(text, expected_n=20):
# 1) Remove code fences
text = re.sub(r"^```.*?\n|\n```$", "", text, flags=re.DOTALL).strip()
# 2) Split lines on numbering
# e.g. "1) ..." or "1. ..." or "1 - ..." etc
candidates = re.split(r"(?:^\s*\d+\s*[\)\.\-\:]\s*)", text, flags=re.MULTILINE)
# The split keeps text fragments; we need to reassemble meaningful lines.
# An easier approach is to capture lines that start with a number:
lines = re.findall(r"^\s*\d+\s*[\)\.\-\:]\s*(.+)$", text, flags=re.MULTILINE)
lines = [l.strip() for l in lines if l.strip()]
# Fallback: if no pattern matched, split by newline and filter bullets
if not lines:
for raw in text.splitlines():
s = raw.strip()
if s and not s.startswith("#"):
lines.append(s)
# Trim to expected_n if overshoot; if undershoot, keep whatever we have
return lines[:expected_n]
# --------------------------------
# 3) ๋ฉ”์ธ ์‘๋‹ต ํ•จ์ˆ˜
# --------------------------------
def respond(
message,
history: list[dict[str, str]],
system_message,
max_tokens,
temperature,
top_p,
# (OAuth ๋ฒ„ํŠผ์€ ์œ ์ง€ํ•˜๋˜, ์•„๋ž˜ ๊ตฌํ˜„์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
hf_token: gr.OAuthToken,
# --- ์ƒํƒœ ๊ฐ’๋“ค ---
phase, # 1 -> 2 -> 3
asked, # ์ง์ „์— ์งˆ๋ฌธ์„ ๋˜์กŒ๋Š”์ง€ ์—ฌ๋ถ€ (True๋ฉด ์ด๋ฒˆ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์€ ๋‹ต๋ณ€์œผ๋กœ ๊ฐ„์ฃผ)
i1, i2, i3, # ๊ฐ ๋‹จ๊ณ„ ์ธ๋ฑ์Šค
gen_questions,# 2๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•  20๊ฐœ ์งˆ๋ฌธ (list[str])
fixed_answers,# 1๋‹จ๊ณ„ ๋‹ต๋ณ€(10๊ฐœ ์ €์žฅ์šฉ)
gen_answers, # 2๋‹จ๊ณ„ ๋‹ต๋ณ€(20๊ฐœ ์ €์žฅ์šฉ)
rep_answers, # 3๋‹จ๊ณ„ ๋‹ต๋ณ€(10๊ฐœ ์ €์žฅ์šฉ)
):
"""
๋Œ€ํ™” ํ๋ฆ„:
- asked == False: ์ด๋ฒˆ ํ˜ธ์ถœ์—์„œ๋Š” '๋‹ค์Œ ์งˆ๋ฌธ'์„ ๋‚ด๋ณด๋‚ด๊ณ  asked=True ๋กœ ์ „ํ™˜
- asked == True : ์ด๋ฒˆ ํ˜ธ์ถœ์˜ message๋ฅผ '๋‹ต๋ณ€'์œผ๋กœ ์ €์žฅํ•˜๊ณ  ์ธ๋ฑ์Šค๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ ๋’ค, ๋‹ค์Œ ์งˆ๋ฌธ์„ ๋‚ด๋ณด๋‚ด๋ฉฐ asked=True ์œ ์ง€
"""
model = "gemini/gemini-2.5-flash"
# ์ตœ์ดˆ ์ง„์ž…(์‚ฌ์šฉ์ž ์ฒซ ๋ฉ”์‹œ์ง€): ์งˆ๋ฌธ์„ ๋˜์งˆ ์ฐจ๋ก€๋กœ ๋งž์ถ˜๋‹ค.
if phase is None:
phase = 1
if asked is None:
asked = False
if i1 is None:
i1 = 0
if i2 is None:
i2 = 0
if i3 is None:
i3 = 0
if gen_questions is None:
gen_questions = []
if fixed_answers is None:
fixed_answers = []
if gen_answers is None:
gen_answers = []
if rep_answers is None:
rep_answers = []
# ํ—ฌํผ: ํ˜„์žฌ ๋‹จ๊ณ„์—์„œ "๋‹ค์Œ ์งˆ๋ฌธ ํ…์ŠคํŠธ"๋ฅผ ๋ฆฌํ„ด
def next_question():
nonlocal phase, i1, i2, i3, gen_questions
if phase == 1:
return FIXED_QUESTIONS[i1] if i1 < len(FIXED_QUESTIONS) else None
elif phase == 2:
return gen_questions[i2] if i2 < len(gen_questions) else None
elif phase == 3:
return FIXED_QUESTIONS[i3] if i3 < len(FIXED_QUESTIONS) else None
return None
# ํ—ฌํผ: 1โ†’2 ๋‹จ๊ณ„ ์ „ํ™˜ ์‹œ ๊ผฌ๋ฆฌ์งˆ๋ฌธ 20๊ฐœ ์ƒ์„ฑ
def ensure_followups():
nonlocal gen_questions
if gen_questions:
return # ์ด๋ฏธ ์ƒ์„ฑ๋จ
# 1๋‹จ๊ณ„ Q/A ํŽ˜์–ด ๊ตฌ์„ฑ
qa_pairs = list(zip(FIXED_QUESTIONS, fixed_answers))
user_prompt = build_followup_user_prompt(qa_pairs)
followup_msgs = [
{"role": "system", "content": FOLLOWUP_SYSTEM},
{"role": "user", "content": user_prompt},
]
res = completion(
model=model,
messages=followup_msgs,
temperature=temperature,
top_p=top_p,
# max_tokens=max_tokens, # ํ•„์š” ์‹œ ํ•ด์ œ
)
res_json = res.choices[0].message.model_dump()
followup_text = res_json["content"].strip()
gen_questions = parse_numbered_list_to_lines(followup_text, expected_n=20)
# ํ˜น์‹œ 20๊ฐœ ๋ฏธ๋งŒ์ด๋ฉด ๋ณด์ถฉ(๊ฐ„๋‹จํ•œ ๋ฐฑ์—…)
while len(gen_questions) < 20:
gen_questions.append(f"(์ถ”๊ฐ€) ๊ด€์‹ฌ ์ฃผ์ œ์— ๋Œ€ํ•ด ๋” ์ž์„ธํžˆ ์„ค๋ช…ํ•ด ์ฃผ์‹ค ์ˆ˜ ์žˆ๋‚˜์š”? [{len(gen_questions)+1}]")
# --------------------------------
# (A) asked == False โ†’ ์งˆ๋ฌธ ๋˜์ง€๊ธฐ
# --------------------------------
if not asked:
if phase == 1 and i1 == 0:
intro = (
"์•ˆ๋…•ํ•˜์„ธ์š”! ๋‹ค์Œ ์ˆœ์„œ๋กœ ์ง„ํ–‰ํ• ๊ฒŒ์š”:\n"
"1) ๊ณ ์ • 10๋ฌธํ•ญ์— ๋จผ์ € ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.\n"
"2) ์ด์–ด์„œ LLM์ด ๋ฐฉ๊ธˆ ๋‹ต๋ณ€์„ ๋ฐ”ํƒ•์œผ๋กœ 20๋ฌธํ•ญ์„ ์ƒ์„ฑํ•ด ์งˆ๋ฌธํ•ฉ๋‹ˆ๋‹ค.\n"
"3) ๋งˆ์ง€๋ง‰์œผ๋กœ ์ฒ˜์Œ์˜ 10๋ฌธํ•ญ์„ ๋‹ค์‹œ ๋ฌป์Šต๋‹ˆ๋‹ค.\n\n"
"๊ทธ๋Ÿผ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!"
)
# ์ฒซ ์•ˆ๋‚ด ํ›„ ์ฒซ ์งˆ๋ฌธ
q = next_question()
asked = True
yield f"{intro}\n\n{q}", phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
# ๊ทธ ์™ธ ์ผ๋ฐ˜ ์ผ€์ด์Šค: ๋‹ค์Œ ์งˆ๋ฌธ
q = next_question()
if q is not None:
asked = True
yield q, phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
# ์งˆ๋ฌธ์ด ๋” ์—†๋‹ค๋ฉด(๋ชจ๋“  ๋‹จ๊ณ„ ์™„๋ฃŒ)
yield "๋ชจ๋“  ์งˆ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฐธ์—ฌํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰", phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
# --------------------------------
# (B) asked == True โ†’ ๋ฐฉ๊ธˆ ๋ฐ›์€ message๋ฅผ ๋‹ต๋ณ€์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๋‹ค์Œ ์งˆ๋ฌธ
# --------------------------------
if asked:
if phase == 1:
# 1๋‹จ๊ณ„ ๋‹ต๋ณ€ ์ €์žฅ
fixed_answers.append(message)
i1 += 1
asked = False # ๋‹ค์Œ ํ„ด์—๋Š” ์งˆ๋ฌธ์„ ๋‚ด๋ณด๋‚ด๋„๋ก
if i1 >= len(FIXED_QUESTIONS):
# 2๋‹จ๊ณ„๋กœ ์ด๋™: ๊ผฌ๋ฆฌ์งˆ๋ฌธ 20๊ฐœ ์ƒ์„ฑ
phase = 2
ensure_followups()
# ๊ณง๋ฐ”๋กœ ๋‹ค์Œ ์งˆ๋ฌธ ๋˜์ง€๊ธฐ
q = next_question()
asked = True
yield f"์ข‹์Šต๋‹ˆ๋‹ค. 1๋‹จ๊ณ„๋ฅผ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค. ์ด์ œ 2๋‹จ๊ณ„(20๋ฌธํ•ญ)๋กœ ๋„˜์–ด๊ฐˆ๊ฒŒ์š”.\n\n{q}", phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
else:
# ๋‹ค์Œ 1๋‹จ๊ณ„ ์งˆ๋ฌธ
q = next_question()
asked = True
yield q, phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
elif phase == 2:
# 2๋‹จ๊ณ„ ๋‹ต๋ณ€ ์ €์žฅ
gen_answers.append(message)
i2 += 1
asked = False
if i2 >= len(gen_questions):
# 3๋‹จ๊ณ„๋กœ ์ด๋™
phase = 3
q = next_question()
asked = True
yield f"์ข‹์•„์š”. 2๋‹จ๊ณ„๋ฅผ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ 1๋‹จ๊ณ„์˜ 10๋ฌธํ•ญ์„ ๋‹ค์‹œ ๋ฌป๊ฒ ์Šต๋‹ˆ๋‹ค.\n\n{q}", phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
else:
q = next_question()
asked = True
yield q, phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
elif phase == 3:
# 3๋‹จ๊ณ„ ๋‹ต๋ณ€ ์ €์žฅ
rep_answers.append(message)
i3 += 1
asked = False
if i3 >= len(FIXED_QUESTIONS):
# ์™„๋ฃŒ
summary = {
"phase1_fixed": [{"q": FIXED_QUESTIONS[i], "a": fixed_answers[i]} for i in range(len(fixed_answers))],
"phase2_generated": [{"q": gen_questions[i], "a": gen_answers[i]} for i in range(len(gen_answers))],
"phase3_repeat": [{"q": FIXED_QUESTIONS[i], "a": rep_answers[i]} for i in range(len(rep_answers))],
}
done_text = (
"๋ชจ๋“  ์งˆ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฐธ์—ฌ์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๐ŸŽ‰\n"
"ํ•„์š”ํ•˜์‹œ๋‹ค๋ฉด ์•„๋ž˜ JSON ์š”์•ฝ์„ ๋ณต์‚ฌํ•ด๊ฐ€์„ธ์š”.\n\n"
+ "```json\n" + json.dumps(summary, ensure_ascii=False, indent=2) + "\n```"
)
yield done_text, phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
else:
q = next_question()
asked = True
yield q, phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
return
# ์•ˆ์ „๋ง (๋„๋‹ฌํ•˜์ง€ ์•Š์•„์•ผ ํ•จ)
yield "์ƒํƒœ ์ „ํ™˜ ์ค‘ ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.", phase, asked, i1, i2, i3, gen_questions, fixed_answers, gen_answers, rep_answers
# --------------------------------
# 4) ChatInterface ๊ตฌ์„ฑ
# --------------------------------
with gr.Blocks() as demo:
with gr.Sidebar():
# ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ์œ ์ง€ (hf_token์€ ํ˜„์žฌ ๊ตฌํ˜„์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ UI๋Š” ๊ทธ๋Œ€๋กœ ๋‘ )
oauth = gr.LoginButton()
gr.Markdown(
"### ์ง„ํ–‰ ์ˆœ์„œ\n"
"1) ๊ณ ์ • 10๋ฌธํ•ญ์— ๋จผ์ € ๋‹ตํ•˜๊ธฐ\n"
"2) LLM์ด ์ƒ์„ฑํ•œ 20๋ฌธํ•ญ ๊ผฌ๋ฆฌ์งˆ๋ฌธ์— ๋‹ตํ•˜๊ธฐ\n"
"3) ๊ณ ์ • 10๋ฌธํ•ญ์„ ๋‹ค์‹œ ํ•œ ๋ฒˆ ๋‹ตํ•˜๊ธฐ"
)
phase = gr.State(1)
asked = gr.State(False)
i1 = gr.State(0)
i2 = gr.State(0)
i3 = gr.State(0)
gen_questions= gr.State([])
fixed_answers= gr.State([])
gen_answers = gr.State([])
rep_answers = gr.State([])
# system/max_tokens/temperature/top_p ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋„ ๋ธ”๋ก ์•ˆ์—์„œ ๋งŒ๋“ค๊ณ  ๋„˜๊น๋‹ˆ๋‹ค
sys_msg = gr.Textbox(value="You are a friendly Chatbot.", label="System message")
max_toks = gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens")
temp = gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature")
top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p (nucleus sampling)")
chatbot = gr.ChatInterface(
respond,
type="messages",
# โœ… ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ: system_message, max_tokens, temperature, top_p, hf_token, (statesโ€ฆ)
additional_inputs=[
sys_msg, max_toks, temp, top_p,
oauth, # <-- โœ… hf_token ์ž๋ฆฌ์— ๋†“๊ธฐ!
phase, asked, i1, i2, i3,
gen_questions, fixed_answers, gen_answers, rep_answers,
],
# โœ… ์ถœ๋ ฅ์—๋„ ๋™์ผํ•œ State ์ธ์Šคํ„ด์Šค ์žฌ์‚ฌ์šฉ
additional_outputs=[
phase, asked, i1, i2, i3,
gen_questions, fixed_answers, gen_answers, rep_answers,
],
)
chatbot.render()
if __name__ == "__main__":
demo.launch()