Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-06-01 16:42:58 +09:00
+108 -48
View File
@@ -8,6 +8,7 @@
YOULBOT_API_TOKEN= ← api.py에 API_TOKEN 설정 시 동일 값 YOULBOT_API_TOKEN= ← api.py에 API_TOKEN 설정 시 동일 값
""" """
import asyncio import asyncio
import html as _html
import os import os
import platform import platform
import subprocess import subprocess
@@ -95,19 +96,21 @@ async def tts_speak(text: str) -> str | None:
async def respond(message, history, show_thinking, user_id, use_tts, run_ids): async def respond(message, history, show_thinking, user_id, use_tts, run_ids):
if not message.strip(): if not message.strip():
yield history, "", None, run_ids, gr.update() yield history, "", None, run_ids, "", ""
return return
history = list(history) history = list(history)
run_ids = list(run_ids) run_ids = list(run_ids)
history.append({"role": "user", "content": message}) history.append({"role": "user", "content": message})
history.append({"role": "assistant", "content": ""}) history.append({"role": "assistant", "content": ""})
yield history, "", None, run_ids, gr.update(value="", visible=False) yield history, "", None, run_ids, "", "" # thinking_box + source_box 초기화
collected_run_id: str | None = None collected_run_id: str | None = None
tts_text = "" # 순수 답변만 누적 (TTS용) tts_text = "" # 순수 답변만 누적 (TTS용)
thinking_acc = "" # 사고 과정 누적 thinking_acc = "" # 전체 누적 (완료 후 details용)
thinking_active = False thinking_text = "" # __thinking 토큰만 (줄 감지용)
thinking_finalized = False
source_box_html = ""
try: try:
async for token, run_id in api_client.chat(message, user_id, show_thinking): async for token, run_id in api_client.chat(message, user_id, show_thinking):
@@ -115,40 +118,54 @@ async def respond(message, history, show_thinking, user_id, use_tts, run_ids):
collected_run_id = run_id collected_run_id = run_id
break break
if isinstance(token, dict) and "__thinking" in token: # 즉시 상태 — thinking_acc에 누적 안 함
thinking_active = True if isinstance(token, dict) and "__status" in token:
thinking_acc += token["__thinking"] if not thinking_acc:
thinking_md = f"🤔 **사고 중...**\n\n{thinking_acc}" yield history, "", None, run_ids, _status_html(token["__status"]), gr.update()
yield history, "", None, run_ids, gr.update(value=thinking_md, visible=True)
continue continue
if thinking_active: # 사고 과정(LLM thinking) — 현재 줄만 live_html로 표시
# 첫 답변 토큰 도착 — 사고 완료 표시 if isinstance(token, dict) and "__thinking" in token:
thinking_active = False thinking_text += token["__thinking"]
yield history, "", None, run_ids, gr.update( thinking_acc += token["__thinking"]
value=f"💭 **사고 완료**\n\n{thinking_acc}", visible=True yield history, "", None, run_ids, _live_html(_last_line(thinking_text)), gr.update()
) continue
# 진행 로그(LangGraph, 검색 등) — 메시지 전체를 live_html로 표시
if isinstance(token, dict) and "__meta" in token: if isinstance(token, dict) and "__meta" in token:
display_token = token["__meta"] thinking_acc += token["__meta"]
else: live = token["__meta"].strip()
display_token = token if live:
tts_text += display_token yield history, "", None, run_ids, _live_html(live), gr.update()
history[-1]["content"] += display_token continue
yield history, "", None, run_ids, gr.update()
# RAG 출처 — 별도 source_box로 표시
if isinstance(token, dict) and "__sources" in token:
source_box_html = _sources_html(token["__sources"])
yield history, "", None, run_ids, gr.update(), source_box_html
continue
# 첫 답변 토큰 도착 — 전체를 details로 전환 (접힌 상태)
if thinking_acc and not thinking_finalized:
thinking_finalized = True
yield history, "", None, run_ids, _thinking_html(thinking_acc), gr.update()
tts_text += token
history[-1]["content"] += token
yield history, "", None, run_ids, gr.update(), gr.update()
except Exception as e: except Exception as e:
history[-1]["content"] += f"\n\n[오류: {e}]" history[-1]["content"] += f"\n\n[오류: {e}]"
yield history, "", None, run_ids, gr.update() yield history, "", None, run_ids, gr.update(), gr.update()
return return
run_ids.append(collected_run_id) run_ids.append(collected_run_id)
if use_tts: if use_tts:
audio_path = await tts_speak(tts_text) audio_path = await tts_speak(tts_text)
yield history, "", audio_path, run_ids, gr.update() yield history, "", audio_path, run_ids, gr.update(), gr.update()
else: else:
yield history, "", None, run_ids, gr.update() yield history, "", None, run_ids, gr.update(), gr.update()
def handle_feedback(like_data: gr.LikeData, history, run_ids, user_id): def handle_feedback(like_data: gr.LikeData, history, run_ids, user_id):
@@ -222,22 +239,68 @@ def delete_doc(source):
# ── UI 구성 ────────────────────────────────────────────────────── # ── UI 구성 ──────────────────────────────────────────────────────
_THINKING_CSS = """ _BOX_STYLE = (
.thinking-box { "background:#f9f9f9;border-left:3px solid #bbb;border-radius:6px;"
background: #f9f9f9; "padding:8px 14px;margin-bottom:6px;"
border-left: 3px solid #bbb; )
border-radius: 6px; _CONTENT_STYLE = (
padding: 10px 14px; "margin-top:6px;white-space:pre-wrap;font-size:0.85em;"
margin-bottom: 6px; "color:#555;max-height:160px;overflow-y:auto;"
max-height: 220px; )
overflow-y: auto;
font-size: 0.85em;
color: #555;
white-space: pre-wrap;
}
"""
with gr.Blocks(title="율봇", css=_THINKING_CSS) as demo:
def _last_line(text: str) -> str:
"""현재 진행 중인 마지막 비어있지 않은 줄 반환."""
lines = [l for l in text.split("\n") if l.strip()]
return lines[-1] if lines else text.strip()
def _live_html(text: str) -> str:
"""스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음)."""
return (
f'<div style="{_BOX_STYLE}">'
f'<strong>🤔 분석 중...</strong>'
f'<div style="{_CONTENT_STYLE}">{_html.escape(text)} ▌</div>'
f'</div>'
)
def _thinking_html(text: str) -> str:
"""완료 후 전체 내용을 접기/펼치기로 표시."""
return (
f'<details style="{_BOX_STYLE}">'
f'<summary style="cursor:pointer;font-weight:bold;">💭 분석 완료</summary>'
f'<div style="{_CONTENT_STYLE}">{_html.escape(text)}</div>'
f'</details>'
)
def _status_html(status: str) -> str:
"""내용 없이 상태만 표시하는 단순 헤더."""
return (
f'<div style="{_BOX_STYLE}">'
f'<strong>🤔 {_html.escape(status)}</strong>'
f'</div>'
)
def _sources_html(sources: list) -> str:
"""RAG 출처 목록을 접기/펼치기로 표시."""
items = "".join(
f"<li>{_html.escape(s['filename'])}"
+ (f"{s['page']}페이지" if "page" in s else "")
+ "</li>"
for s in sources
)
return (
f'<details style="{_BOX_STYLE}">'
f'<summary style="cursor:pointer;font-weight:bold;">📄 출처 ({len(sources)}개)</summary>'
f'<ul style="margin:6px 0;padding-left:18px;font-size:0.85em;color:#555;">{items}</ul>'
f'</details>'
)
with gr.Blocks(title="율봇") as demo:
gr.Markdown("# 율봇\n육아·금융 전문 AI 상담 도우미") gr.Markdown("# 율봇\n육아·금융 전문 AI 상담 도우미")
user_state = gr.State(DEFAULT_USER) user_state = gr.State(DEFAULT_USER)
@@ -252,12 +315,9 @@ with gr.Blocks(title="율봇", css=_THINKING_CSS) as demo:
scale=1, scale=1,
) )
thinking_box = gr.Markdown( thinking_box = gr.HTML(value="")
value="",
visible=False,
elem_classes=["thinking-box"],
)
chatbot = gr.Chatbot(label="율봇", height=500) chatbot = gr.Chatbot(label="율봇", height=500)
source_box = gr.HTML(value="")
with gr.Row(): with gr.Row():
msg_box = gr.Textbox( msg_box = gr.Textbox(
placeholder="질문을 입력하세요... (Enter로 전송)", placeholder="질문을 입력하세요... (Enter로 전송)",
@@ -277,7 +337,7 @@ with gr.Blocks(title="율봇", css=_THINKING_CSS) as demo:
transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1) transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1)
with gr.Row(): with gr.Row():
show_thinking = gr.Checkbox(label="사고 과정 표시", value=False) show_thinking = gr.Checkbox(label="사고 과정 표시", value=True)
use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False) use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False)
reset_btn = gr.Button("대화 초기화", size="sm") reset_btn = gr.Button("대화 초기화", size="sm")
@@ -297,12 +357,12 @@ with gr.Blocks(title="율봇", css=_THINKING_CSS) as demo:
send_btn.click( send_btn.click(
respond, respond,
inputs=[msg_box, chatbot, show_thinking, user_state, use_tts, run_ids_state], inputs=[msg_box, chatbot, show_thinking, user_state, use_tts, run_ids_state],
outputs=[chatbot, msg_box, tts_output, run_ids_state, thinking_box], outputs=[chatbot, msg_box, tts_output, run_ids_state, thinking_box, source_box],
) )
msg_box.submit( msg_box.submit(
respond, respond,
inputs=[msg_box, chatbot, show_thinking, user_state, use_tts, run_ids_state], inputs=[msg_box, chatbot, show_thinking, user_state, use_tts, run_ids_state],
outputs=[chatbot, msg_box, tts_output, run_ids_state, thinking_box], outputs=[chatbot, msg_box, tts_output, run_ids_state, thinking_box, source_box],
) )
reset_btn.click(reset_chat, inputs=[user_state], outputs=[chatbot, run_ids_state]) reset_btn.click(reset_chat, inputs=[user_state], outputs=[chatbot, run_ids_state])