june-woo commited on
Commit
aa662d8
·
1 Parent(s): 9d6f358

gradio app 페르소나 탭 추가 #7

Browse files
Files changed (2) hide show
  1. api_server.py +28 -0
  2. 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={"query": query},
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
- payload = json.loads(payload_text)
117
  except json.JSONDecodeError:
118
  continue
119
 
120
- event_queue.put(("event", payload))
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.Row():
437
- with gr.Column(scale=3):
438
- endpoint = gr.Textbox(
439
- label="SSE Endpoint",
440
- value=default_endpoint,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  )
442
- query = gr.Textbox(
443
- label="질문",
444
- lines=3,
445
- value=EXAMPLE_QUERIES[0],
 
 
 
 
 
 
 
 
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
- meta = gr.Code(label="최종 결과 (JSON)", language="json", elem_id="meta-box")
 
 
 
 
 
 
462
 
463
- gr.HTML(AUTO_SCROLL_SCRIPT, visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
- run_btn.click(
466
- fn=stream_analyze,
467
- inputs=[query, endpoint],
468
- outputs=[answer, timer, meta],
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