Phase 25: Separate RAG sources into collapsible box below chatbot
- Add source_box gr.HTML component below chatbot - Add _sources_html() helper rendering <details> expand/collapse - Handle __sources token in respond(): update source_box independently of thinking_box - Reset both thinking_box and source_box on each new message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,20 +96,21 @@ async def tts_speak(text: str) -> str | None:
|
||||
|
||||
async def respond(message, history, show_thinking, user_id, use_tts, run_ids):
|
||||
if not message.strip():
|
||||
yield history, "", None, run_ids, ""
|
||||
yield history, "", None, run_ids, "", ""
|
||||
return
|
||||
|
||||
history = list(history)
|
||||
run_ids = list(run_ids)
|
||||
history.append({"role": "user", "content": message})
|
||||
history.append({"role": "assistant", "content": ""})
|
||||
yield history, "", None, run_ids, "" # thinking_box 초기화
|
||||
yield history, "", None, run_ids, "", "" # thinking_box + source_box 초기화
|
||||
|
||||
collected_run_id: str | None = None
|
||||
tts_text = "" # 순수 답변만 누적 (TTS용)
|
||||
thinking_acc = "" # 전체 누적 (완료 후 details용)
|
||||
thinking_text = "" # __thinking 토큰만 (줄 감지용)
|
||||
thinking_finalized = False
|
||||
source_box_html = ""
|
||||
|
||||
try:
|
||||
async for token, run_id in api_client.chat(message, user_id, show_thinking):
|
||||
@@ -120,14 +121,14 @@ async def respond(message, history, show_thinking, user_id, use_tts, run_ids):
|
||||
# 즉시 상태 — thinking_acc에 누적 안 함
|
||||
if isinstance(token, dict) and "__status" in token:
|
||||
if not thinking_acc:
|
||||
yield history, "", None, run_ids, _status_html(token["__status"])
|
||||
yield history, "", None, run_ids, _status_html(token["__status"]), gr.update()
|
||||
continue
|
||||
|
||||
# 사고 과정(LLM thinking) — 현재 줄만 live_html로 표시
|
||||
if isinstance(token, dict) and "__thinking" in token:
|
||||
thinking_text += token["__thinking"]
|
||||
thinking_acc += token["__thinking"]
|
||||
yield history, "", None, run_ids, _live_html(_last_line(thinking_text))
|
||||
yield history, "", None, run_ids, _live_html(_last_line(thinking_text)), gr.update()
|
||||
continue
|
||||
|
||||
# 진행 로그(LangGraph, 검색 등) — 메시지 전체를 live_html로 표시
|
||||
@@ -135,30 +136,36 @@ async def respond(message, history, show_thinking, user_id, use_tts, run_ids):
|
||||
thinking_acc += token["__meta"]
|
||||
live = token["__meta"].strip()
|
||||
if live:
|
||||
yield history, "", None, run_ids, _live_html(live)
|
||||
yield history, "", None, run_ids, _live_html(live), gr.update()
|
||||
continue
|
||||
|
||||
# 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)
|
||||
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()
|
||||
yield history, "", None, run_ids, gr.update(), gr.update()
|
||||
|
||||
except Exception as 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
|
||||
|
||||
run_ids.append(collected_run_id)
|
||||
|
||||
if use_tts:
|
||||
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:
|
||||
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):
|
||||
@@ -277,6 +284,22 @@ def _status_html(status: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
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 상담 도우미")
|
||||
|
||||
@@ -294,6 +317,7 @@ with gr.Blocks(title="율봇") as demo:
|
||||
|
||||
thinking_box = gr.HTML(value="")
|
||||
chatbot = gr.Chatbot(label="율봇", height=500)
|
||||
source_box = gr.HTML(value="")
|
||||
with gr.Row():
|
||||
msg_box = gr.Textbox(
|
||||
placeholder="질문을 입력하세요... (Enter로 전송)",
|
||||
@@ -333,12 +357,12 @@ with gr.Blocks(title="율봇") as demo:
|
||||
send_btn.click(
|
||||
respond,
|
||||
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(
|
||||
respond,
|
||||
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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user