gradio app 페르소나 탭 추가 #7
Browse files- api_server.py +28 -0
- gradio_app.py +195 -44
api_server.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import json
|
| 2 |
from queue import Empty, Queue
|
| 3 |
from threading import Thread
|
|
|
|
| 4 |
|
| 5 |
from fastapi import FastAPI
|
| 6 |
from fastapi.encoders import jsonable_encoder
|
|
@@ -8,12 +9,35 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
| 10 |
from pipeline import pipeline as run_pipeline
|
|
|
|
| 11 |
|
| 12 |
app = FastAPI()
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
class QueryRequest(BaseModel):
|
| 15 |
query: str
|
| 16 |
stream: bool = True
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
def _sse(payload: dict) -> str:
|
|
@@ -25,9 +49,12 @@ async def analyze(request: QueryRequest):
|
|
| 25 |
query = (request.query or "").strip()
|
| 26 |
stream = request.stream
|
| 27 |
|
|
|
|
|
|
|
| 28 |
if not stream:
|
| 29 |
result = run_pipeline(
|
| 30 |
query,
|
|
|
|
| 31 |
status_callback=None,
|
| 32 |
stream_callback=None,
|
| 33 |
stream=False,
|
|
@@ -60,6 +87,7 @@ async def analyze(request: QueryRequest):
|
|
| 60 |
try:
|
| 61 |
result = run_pipeline(
|
| 62 |
query,
|
|
|
|
| 63 |
status_callback=on_status,
|
| 64 |
stream_callback=on_delta if stream else None,
|
| 65 |
stream=stream,
|
|
|
|
| 1 |
import json
|
| 2 |
from queue import Empty, Queue
|
| 3 |
from threading import Thread
|
| 4 |
+
from typing import Optional
|
| 5 |
|
| 6 |
from fastapi import FastAPI
|
| 7 |
from fastapi.encoders import jsonable_encoder
|
|
|
|
| 9 |
from pydantic import BaseModel
|
| 10 |
|
| 11 |
from pipeline import pipeline as run_pipeline
|
| 12 |
+
from persona.make_persona import make_persona
|
| 13 |
|
| 14 |
app = FastAPI()
|
| 15 |
|
| 16 |
+
|
| 17 |
+
class PersonaRequest(BaseModel):
|
| 18 |
+
info: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@app.post("/persona/")
|
| 22 |
+
async def create_persona(request: PersonaRequest):
|
| 23 |
+
info = (request.info or "").strip()
|
| 24 |
+
if not info:
|
| 25 |
+
return JSONResponse(status_code=400, content={"error": "info 필드가 비어 있습니다."})
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
persona = make_persona(info)
|
| 29 |
+
except Exception as exc:
|
| 30 |
+
return JSONResponse(status_code=500, content={"error": str(exc)})
|
| 31 |
+
|
| 32 |
+
if persona is None:
|
| 33 |
+
return JSONResponse(status_code=500, content={"error": "페르소나 생성에 실패했습니다."})
|
| 34 |
+
|
| 35 |
+
return JSONResponse(content=persona.model_dump())
|
| 36 |
+
|
| 37 |
class QueryRequest(BaseModel):
|
| 38 |
query: str
|
| 39 |
stream: bool = True
|
| 40 |
+
persona_name: Optional[str] = None
|
| 41 |
|
| 42 |
|
| 43 |
def _sse(payload: dict) -> str:
|
|
|
|
| 49 |
query = (request.query or "").strip()
|
| 50 |
stream = request.stream
|
| 51 |
|
| 52 |
+
persona_name = (request.persona_name or "").strip() or None
|
| 53 |
+
|
| 54 |
if not stream:
|
| 55 |
result = run_pipeline(
|
| 56 |
query,
|
| 57 |
+
persona_name=persona_name,
|
| 58 |
status_callback=None,
|
| 59 |
stream_callback=None,
|
| 60 |
stream=False,
|
|
|
|
| 87 |
try:
|
| 88 |
result = run_pipeline(
|
| 89 |
query,
|
| 90 |
+
persona_name=persona_name,
|
| 91 |
status_callback=on_status,
|
| 92 |
stream_callback=on_delta if stream else None,
|
| 93 |
stream=stream,
|
gradio_app.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
| 1 |
import argparse
|
| 2 |
import html as html_lib
|
| 3 |
import json
|
|
|
|
| 4 |
import time
|
|
|
|
| 5 |
from queue import Empty, Queue
|
| 6 |
from threading import Thread
|
| 7 |
-
from typing import Generator, Tuple
|
| 8 |
|
| 9 |
import gradio as gr
|
| 10 |
import requests
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
| 13 |
EXAMPLE_QUERIES = [
|
| 14 |
"AAPL의 최근 실적과 투자 포인트 요약해줘",
|
| 15 |
"TSLA의 기술적 분석 리포트 작성",
|
|
@@ -66,11 +70,89 @@ def timer_text(elapsed: str) -> str:
|
|
| 66 |
return f"⏱ {elapsed}"
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def stream_analyze(
|
| 70 |
-
query: str, endpoint: str
|
| 71 |
) -> Generator[Tuple[str, str, str], None, None]:
|
| 72 |
query = (query or "").strip()
|
| 73 |
endpoint = (endpoint or "").strip()
|
|
|
|
| 74 |
|
| 75 |
if not query:
|
| 76 |
yield loading_markdown("질문을 입력해주세요."), timer_text("0.0초"), ""
|
|
@@ -94,9 +176,13 @@ def stream_analyze(
|
|
| 94 |
|
| 95 |
def reader_worker() -> None:
|
| 96 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
with requests.post(
|
| 98 |
endpoint,
|
| 99 |
-
json=
|
| 100 |
headers={"Accept": "text/event-stream"},
|
| 101 |
stream=True,
|
| 102 |
timeout=(10, 300),
|
|
@@ -113,11 +199,11 @@ def stream_analyze(
|
|
| 113 |
|
| 114 |
payload_text = line[5:].strip()
|
| 115 |
try:
|
| 116 |
-
|
| 117 |
except json.JSONDecodeError:
|
| 118 |
continue
|
| 119 |
|
| 120 |
-
event_queue.put(("event",
|
| 121 |
except requests.exceptions.ConnectionError:
|
| 122 |
event_queue.put(("exception", f"연결 실패: {endpoint} 확인"))
|
| 123 |
except requests.exceptions.Timeout:
|
|
@@ -192,7 +278,6 @@ def stream_analyze(
|
|
| 192 |
if worker_finished and terminal_event:
|
| 193 |
break
|
| 194 |
if worker_finished and not terminal_event:
|
| 195 |
-
# 서버가 done 이벤트 없이 종료된 경우 마지막 화면 갱신 후 종료
|
| 196 |
if not first_delta_received:
|
| 197 |
loading_msg = "연결 ��료"
|
| 198 |
yield loading_markdown(loading_msg), timer_text(elapsed_str()), meta_text
|
|
@@ -420,6 +505,17 @@ def create_app(default_endpoint: str) -> gr.Blocks:
|
|
| 420 |
max-height: 300px;
|
| 421 |
overflow-y: auto;
|
| 422 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
"""
|
| 424 |
|
| 425 |
theme = gr.themes.Soft(
|
|
@@ -433,49 +529,104 @@ def create_app(default_endpoint: str) -> gr.Blocks:
|
|
| 433 |
gr.Markdown("## 📈 Wallstreet-AI")
|
| 434 |
gr.Markdown("A finance AI that combines earnings, news, and market trends in one place.")
|
| 435 |
|
| 436 |
-
with gr.
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
)
|
| 442 |
-
query
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
)
|
| 447 |
-
with gr.Row():
|
| 448 |
-
run_btn = gr.Button("🔍 질문하기", variant="primary", scale=3)
|
| 449 |
-
clear_btn = gr.Button("🗑 초기화", scale=1)
|
| 450 |
-
|
| 451 |
-
with gr.Column(scale=1):
|
| 452 |
-
gr.Markdown("**Example Questions**")
|
| 453 |
-
for ex in EXAMPLE_QUERIES:
|
| 454 |
-
gr.Button(ex, size="sm").click(
|
| 455 |
-
fn=lambda x=ex: x, outputs=query
|
| 456 |
-
)
|
| 457 |
-
|
| 458 |
-
answer = gr.Markdown(value=to_markdown(""), label="답변", elem_id="answer-wrapper")
|
| 459 |
-
timer = gr.Markdown(value=timer_text("0.0초"), elem_id="timer-row")
|
| 460 |
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
query.submit(
|
| 471 |
-
fn=stream_analyze,
|
| 472 |
-
inputs=[query, endpoint],
|
| 473 |
-
outputs=[answer, timer, meta],
|
| 474 |
-
)
|
| 475 |
-
clear_btn.click(
|
| 476 |
-
fn=lambda: (to_markdown(""), timer_text("0.0초"), ""),
|
| 477 |
-
outputs=[answer, timer, meta],
|
| 478 |
-
)
|
| 479 |
|
| 480 |
return demo
|
| 481 |
|
|
|
|
| 1 |
import argparse
|
| 2 |
import html as html_lib
|
| 3 |
import json
|
| 4 |
+
import os
|
| 5 |
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
from queue import Empty, Queue
|
| 8 |
from threading import Thread
|
| 9 |
+
from typing import Generator, List, Tuple
|
| 10 |
|
| 11 |
import gradio as gr
|
| 12 |
import requests
|
| 13 |
|
| 14 |
|
| 15 |
+
PERSONA_FILE = Path("persona.jsonl")
|
| 16 |
+
|
| 17 |
EXAMPLE_QUERIES = [
|
| 18 |
"AAPL의 최근 실적과 투자 포인트 요약해줘",
|
| 19 |
"TSLA의 기술적 분석 리포트 작성",
|
|
|
|
| 70 |
return f"⏱ {elapsed}"
|
| 71 |
|
| 72 |
|
| 73 |
+
def load_persona_choices() -> List[str]:
|
| 74 |
+
"""persona.jsonl에서 persona 이름 목록 로드"""
|
| 75 |
+
choices = ["없음"]
|
| 76 |
+
if PERSONA_FILE.exists():
|
| 77 |
+
with PERSONA_FILE.open("r", encoding="utf-8") as f:
|
| 78 |
+
for line in f:
|
| 79 |
+
line = line.strip()
|
| 80 |
+
if not line:
|
| 81 |
+
continue
|
| 82 |
+
try:
|
| 83 |
+
data = json.loads(line)
|
| 84 |
+
name = data.get("name", "")
|
| 85 |
+
if name and name not in choices:
|
| 86 |
+
choices.append(name)
|
| 87 |
+
except json.JSONDecodeError:
|
| 88 |
+
continue
|
| 89 |
+
return choices
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def make_persona_gradio(info: str, endpoint: str):
|
| 93 |
+
# API 서버 /persona/ 를 호출하여 persona 생성
|
| 94 |
+
if not info or not info.strip():
|
| 95 |
+
return "인물 정보를 입력해주세요.", "{}"
|
| 96 |
+
|
| 97 |
+
persona_endpoint = endpoint.rstrip("/").rsplit("/", 1)[0] + "/persona/"
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
resp = requests.post(
|
| 101 |
+
persona_endpoint,
|
| 102 |
+
json={"info": info.strip()},
|
| 103 |
+
timeout=(10, 300),
|
| 104 |
+
)
|
| 105 |
+
resp.raise_for_status()
|
| 106 |
+
data = resp.json()
|
| 107 |
+
except requests.exceptions.ConnectionError:
|
| 108 |
+
return f"연결 실패: {persona_endpoint} 확인", "{}"
|
| 109 |
+
except requests.exceptions.Timeout:
|
| 110 |
+
return "요청 시간 초과", "{}"
|
| 111 |
+
except requests.RequestException as exc:
|
| 112 |
+
return f"요청 실패: {exc}", "{}"
|
| 113 |
+
|
| 114 |
+
# persona.jsonl에 저장 (중복 이름 제외)
|
| 115 |
+
existing_names = []
|
| 116 |
+
if PERSONA_FILE.exists():
|
| 117 |
+
with PERSONA_FILE.open("r", encoding="utf-8") as f:
|
| 118 |
+
for line in f:
|
| 119 |
+
line = line.strip()
|
| 120 |
+
if not line:
|
| 121 |
+
continue
|
| 122 |
+
try:
|
| 123 |
+
existing_names.append(json.loads(line).get("name", ""))
|
| 124 |
+
except json.JSONDecodeError:
|
| 125 |
+
pass
|
| 126 |
+
|
| 127 |
+
if data.get("name") and data["name"] not in existing_names:
|
| 128 |
+
with PERSONA_FILE.open("a", encoding="utf-8") as f:
|
| 129 |
+
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
| 130 |
+
|
| 131 |
+
result_md = f"""**이름**: {data.get('name', '')}
|
| 132 |
+
|
| 133 |
+
**배경**: {data.get('background', '')}
|
| 134 |
+
|
| 135 |
+
**금융 사고 방식**: {data.get('financial_mindset', '')}
|
| 136 |
+
|
| 137 |
+
**데이터 분석 방식**: {data.get('data_analysis_approach', '')}
|
| 138 |
+
|
| 139 |
+
**답변 스타일**: {data.get('response_style', '')}
|
| 140 |
+
|
| 141 |
+
**핵심 원칙**: {', '.join(data.get('key_principles', []))}
|
| 142 |
+
"""
|
| 143 |
+
quotes = data.get("famous_quotes") or []
|
| 144 |
+
if quotes:
|
| 145 |
+
result_md += f"\n**어록**: {' / '.join(quotes)}"
|
| 146 |
+
|
| 147 |
+
return result_md, json.dumps(data, ensure_ascii=False, indent=2)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
def stream_analyze(
|
| 151 |
+
query: str, persona_name: str, endpoint: str
|
| 152 |
) -> Generator[Tuple[str, str, str], None, None]:
|
| 153 |
query = (query or "").strip()
|
| 154 |
endpoint = (endpoint or "").strip()
|
| 155 |
+
persona_name = (persona_name or "").strip()
|
| 156 |
|
| 157 |
if not query:
|
| 158 |
yield loading_markdown("질문을 입력해주세요."), timer_text("0.0초"), ""
|
|
|
|
| 176 |
|
| 177 |
def reader_worker() -> None:
|
| 178 |
try:
|
| 179 |
+
payload = {"query": query}
|
| 180 |
+
if persona_name and persona_name != "없음":
|
| 181 |
+
payload["persona_name"] = persona_name
|
| 182 |
+
|
| 183 |
with requests.post(
|
| 184 |
endpoint,
|
| 185 |
+
json=payload,
|
| 186 |
headers={"Accept": "text/event-stream"},
|
| 187 |
stream=True,
|
| 188 |
timeout=(10, 300),
|
|
|
|
| 199 |
|
| 200 |
payload_text = line[5:].strip()
|
| 201 |
try:
|
| 202 |
+
parsed = json.loads(payload_text)
|
| 203 |
except json.JSONDecodeError:
|
| 204 |
continue
|
| 205 |
|
| 206 |
+
event_queue.put(("event", parsed))
|
| 207 |
except requests.exceptions.ConnectionError:
|
| 208 |
event_queue.put(("exception", f"연결 실패: {endpoint} 확인"))
|
| 209 |
except requests.exceptions.Timeout:
|
|
|
|
| 278 |
if worker_finished and terminal_event:
|
| 279 |
break
|
| 280 |
if worker_finished and not terminal_event:
|
|
|
|
| 281 |
if not first_delta_received:
|
| 282 |
loading_msg = "연결 ��료"
|
| 283 |
yield loading_markdown(loading_msg), timer_text(elapsed_str()), meta_text
|
|
|
|
| 505 |
max-height: 300px;
|
| 506 |
overflow-y: auto;
|
| 507 |
}
|
| 508 |
+
|
| 509 |
+
#persona-result-wrapper {
|
| 510 |
+
min-height: 200px;
|
| 511 |
+
max-height: 50vh;
|
| 512 |
+
overflow-y: auto !important;
|
| 513 |
+
border: 1px solid var(--ws-border) !important;
|
| 514 |
+
border-radius: 14px !important;
|
| 515 |
+
background: var(--ws-surface) !important;
|
| 516 |
+
padding: 16px 20px !important;
|
| 517 |
+
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.06) !important;
|
| 518 |
+
}
|
| 519 |
"""
|
| 520 |
|
| 521 |
theme = gr.themes.Soft(
|
|
|
|
| 529 |
gr.Markdown("## 📈 Wallstreet-AI")
|
| 530 |
gr.Markdown("A finance AI that combines earnings, news, and market trends in one place.")
|
| 531 |
|
| 532 |
+
with gr.Tabs():
|
| 533 |
+
# ── Tab 1: 질문하기 ──────────────────────────────────────
|
| 534 |
+
with gr.Tab("💬 질문하기"):
|
| 535 |
+
with gr.Row():
|
| 536 |
+
with gr.Column(scale=3):
|
| 537 |
+
endpoint = gr.Textbox(
|
| 538 |
+
label="SSE Endpoint",
|
| 539 |
+
value=default_endpoint,
|
| 540 |
+
)
|
| 541 |
+
persona_dropdown = gr.Dropdown(
|
| 542 |
+
label="페르소나 선택",
|
| 543 |
+
choices=load_persona_choices(),
|
| 544 |
+
value="없음",
|
| 545 |
+
interactive=True,
|
| 546 |
+
)
|
| 547 |
+
refresh_btn = gr.Button("🔄 페르소나 목록 새로고침", size="sm")
|
| 548 |
+
query = gr.Textbox(
|
| 549 |
+
label="질문",
|
| 550 |
+
lines=3,
|
| 551 |
+
value=EXAMPLE_QUERIES[0],
|
| 552 |
+
)
|
| 553 |
+
with gr.Row():
|
| 554 |
+
run_btn = gr.Button("🔍 질문하기", variant="primary", scale=3)
|
| 555 |
+
clear_btn = gr.Button("🗑 초기화", scale=1)
|
| 556 |
+
|
| 557 |
+
with gr.Column(scale=1):
|
| 558 |
+
gr.Markdown("**Example Questions**")
|
| 559 |
+
for ex in EXAMPLE_QUERIES:
|
| 560 |
+
gr.Button(ex, size="sm").click(
|
| 561 |
+
fn=lambda x=ex: x, outputs=query
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
answer = gr.Markdown(value=to_markdown(""), label="답변", elem_id="answer-wrapper")
|
| 565 |
+
timer = gr.Markdown(value=timer_text("0.0초"), elem_id="timer-row")
|
| 566 |
+
meta = gr.Code(label="최종 결과 (JSON)", language="json", elem_id="meta-box")
|
| 567 |
+
|
| 568 |
+
gr.HTML(AUTO_SCROLL_SCRIPT, visible=False)
|
| 569 |
+
|
| 570 |
+
run_btn.click(
|
| 571 |
+
fn=stream_analyze,
|
| 572 |
+
inputs=[query, persona_dropdown, endpoint],
|
| 573 |
+
outputs=[answer, timer, meta],
|
| 574 |
)
|
| 575 |
+
query.submit(
|
| 576 |
+
fn=stream_analyze,
|
| 577 |
+
inputs=[query, persona_dropdown, endpoint],
|
| 578 |
+
outputs=[answer, timer, meta],
|
| 579 |
+
)
|
| 580 |
+
clear_btn.click(
|
| 581 |
+
fn=lambda: (to_markdown(""), timer_text("0.0초"), ""),
|
| 582 |
+
outputs=[answer, timer, meta],
|
| 583 |
+
)
|
| 584 |
+
refresh_btn.click(
|
| 585 |
+
fn=lambda: gr.Dropdown(choices=load_persona_choices(), value="없음"),
|
| 586 |
+
outputs=[persona_dropdown],
|
| 587 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
+
# ── Tab 2: 페르소나 만들기 ────────────────────────────────
|
| 590 |
+
with gr.Tab("🧑💼 페르소나 만들기"):
|
| 591 |
+
gr.Markdown("### 새 페르소나 생성")
|
| 592 |
+
gr.Markdown(
|
| 593 |
+
"금융 인물의 이름이나 설명을 입력하면 AI가 해�� 인물의 금융 사고방식, "
|
| 594 |
+
"분석 스타일, 답변 스타일을 자동으로 생성합니다. "
|
| 595 |
+
)
|
| 596 |
|
| 597 |
+
with gr.Row():
|
| 598 |
+
with gr.Column(scale=2):
|
| 599 |
+
persona_info_input = gr.Textbox(
|
| 600 |
+
label="인물 정보",
|
| 601 |
+
placeholder="예: 워렌 버핏, JP모건, 가타야마 아키라 ...",
|
| 602 |
+
lines=3,
|
| 603 |
+
)
|
| 604 |
+
persona_gen_btn = gr.Button("✨ 페르소나 생성", variant="primary")
|
| 605 |
+
|
| 606 |
+
with gr.Column(scale=1):
|
| 607 |
+
gr.Markdown("**예시 인물**")
|
| 608 |
+
example_personas = ["워렌 버핏", "JP모건", "가타야마 아키라"]
|
| 609 |
+
for ep in example_personas:
|
| 610 |
+
gr.Button(ep, size="sm").click(
|
| 611 |
+
fn=lambda x=ep: x, outputs=persona_info_input
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
persona_result_md = gr.Markdown(
|
| 615 |
+
value="",
|
| 616 |
+
label="생성 결과",
|
| 617 |
+
elem_id="persona-result-wrapper",
|
| 618 |
+
)
|
| 619 |
+
persona_result_json = gr.Code(
|
| 620 |
+
label="페르소나 JSON",
|
| 621 |
+
language="json",
|
| 622 |
+
elem_id="meta-box",
|
| 623 |
+
)
|
| 624 |
|
| 625 |
+
persona_gen_btn.click(
|
| 626 |
+
fn=make_persona_gradio,
|
| 627 |
+
inputs=[persona_info_input, endpoint],
|
| 628 |
+
outputs=[persona_result_md, persona_result_json],
|
| 629 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
|
| 631 |
return demo
|
| 632 |
|