davidkim205 commited on
Commit
fee71d0
Β·
2 Parent(s): 1f69a500072475

Merge pull request #16 from davidkim205/branch/15

Browse files
api_server.py CHANGED
@@ -83,23 +83,110 @@ sys.stdout = _stdout_proxy
83
 
84
  class PersonaRequest(BaseModel):
85
  info: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
 
88
  @app.post("/persona/")
89
  async def create_persona(request: PersonaRequest):
90
  info = (request.info or "").strip()
 
 
91
  if not info:
92
  return JSONResponse(status_code=400, content={"error": "info ν•„λ“œκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€."})
93
 
94
- try:
95
- persona = make_persona(info)
96
- except Exception as exc:
97
- return JSONResponse(status_code=500, content={"error": str(exc)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- if persona is None:
100
- return JSONResponse(status_code=500, content={"error": "페λ₯΄μ†Œλ‚˜ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."})
 
 
 
 
 
 
 
101
 
102
- return JSONResponse(content=persona.model_dump())
 
 
 
 
 
103
 
104
  class QueryRequest(BaseModel):
105
  query: str
 
83
 
84
  class PersonaRequest(BaseModel):
85
  info: str
86
+ stream: bool = True
87
+
88
+
89
+ PERSONA_STATUS_MESSAGES = [
90
+ "인물 정보 μˆ˜μ§‘ 쀑...",
91
+ "μ›Ή 검색을 톡해 λ°°κ²½ 쑰사 쀑...",
92
+ "금육 사고 방식 뢄석 쀑...",
93
+ "데이터 뢄석 접근법 평가 쀑...",
94
+ "λ‹΅λ³€ μŠ€νƒ€μΌ νŠΉμ„± νŒŒμ•… 쀑...",
95
+ "핡심 투자 원칙 μΆ”μΆœ 쀑...",
96
+ "λŒ€ν‘œ 어둝 정리 쀑...",
97
+ "페λ₯΄μ†Œλ‚˜ ν”„λ‘œν•„ ꡬ성 쀑...",
98
+ "μ΅œμ’… 검증 및 μ €μž₯ μ€€λΉ„ 쀑...",
99
+ ]
100
+
101
+
102
+ def _build_persona_payload(persona) -> dict:
103
+ return {
104
+ "type": "result",
105
+ "name": persona.name,
106
+ "full_name": persona.full_name,
107
+ "summary": persona.summary,
108
+ "financial_mindset": persona.financial_mindset,
109
+ "data_analysis_approach": persona.data_analysis_approach,
110
+ "response_style": persona.response_style,
111
+ "key_principles": persona.key_principles,
112
+ "famous_quotes": getattr(persona, "famous_quotes", None),
113
+ }
114
 
115
 
116
  @app.post("/persona/")
117
  async def create_persona(request: PersonaRequest):
118
  info = (request.info or "").strip()
119
+ stream = request.stream
120
+
121
  if not info:
122
  return JSONResponse(status_code=400, content={"error": "info ν•„λ“œκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€."})
123
 
124
+ if not stream:
125
+ try:
126
+ persona = make_persona(info)
127
+ except Exception as exc:
128
+ return JSONResponse(status_code=500, content={"error": str(exc)})
129
+
130
+ if persona is None:
131
+ return JSONResponse(status_code=500, content={"error": "페λ₯΄μ†Œλ‚˜ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."})
132
+
133
+ return JSONResponse(content=persona.model_dump())
134
+
135
+ def event_stream():
136
+ event_queue: Queue = Queue()
137
+
138
+ def status_sender():
139
+ import asyncio
140
+
141
+ async def send_status():
142
+ for i, message in enumerate(PERSONA_STATUS_MESSAGES[:-1]): # λ§ˆμ§€λ§‰ λ©”μ‹œμ§€λŠ” μ™„λ£Œ μ‹œμ μ— μ‚¬μš©
143
+ event_queue.put({"type": "status", "message": message})
144
+ await asyncio.sleep(8)
145
+
146
+ # 비동기 이벀트 λ£¨ν”„μ—μ„œ μ‹€ν–‰
147
+ loop = asyncio.new_event_loop()
148
+ asyncio.set_event_loop(loop)
149
+ loop.run_until_complete(send_status())
150
+
151
+ def worker():
152
+ thread_id = threading.get_ident()
153
+ _stdout_proxy.register(thread_id, _QueueingStdoutTee(_stdout_proxy._target, event_queue))
154
+ try:
155
+ # status λ©”μ‹œμ§€ 전솑 μŠ€λ ˆλ“œ μ‹œμž‘
156
+ status_thread = Thread(target=status_sender, daemon=True)
157
+ status_thread.start()
158
+
159
+ persona = make_persona(info)
160
+
161
+ if persona is None:
162
+ event_queue.put({"type": "error", "message": "페λ₯΄μ†Œλ‚˜ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."})
163
+ else:
164
+ event_queue.put(_build_persona_payload(persona))
165
+ except Exception as exc:
166
+ event_queue.put({"type": "error", "message": str(exc)})
167
+ finally:
168
+ _stdout_proxy.unregister(thread_id)
169
+ event_queue.put({"type": "done"})
170
+
171
+ yield _sse({"type": "status", "message": "페λ₯΄μ†Œλ‚˜ 생성 μ€€λΉ„ 쀑..."})
172
+ Thread(target=worker, daemon=True).start()
173
 
174
+ done = False
175
+ while not done:
176
+ try:
177
+ event = event_queue.get(timeout=0.2)
178
+ except Empty:
179
+ continue
180
+ yield _sse(jsonable_encoder(event))
181
+ if event.get("type") == "done":
182
+ done = True
183
 
184
+ headers = {
185
+ "Cache-Control": "no-cache",
186
+ "Connection": "keep-alive",
187
+ "X-Accel-Buffering": "no",
188
+ }
189
+ return StreamingResponse(event_stream(), media_type="text/event-stream", headers=headers)
190
 
191
  class QueryRequest(BaseModel):
192
  query: str
gradio_app.py CHANGED
@@ -126,7 +126,7 @@ def _status_icon(msg):
126
  class PersonaLine(BaseModel):
127
  name: str
128
  full_name: str
129
- background: str
130
  financial_mindset: str
131
  data_analysis_approach: str
132
  response_style: str
@@ -169,6 +169,9 @@ def _parse_personas():
169
  continue
170
  if not data.get("full_name"):
171
  data["full_name"] = data.get("name", "")
 
 
 
172
  personas.append(PersonaLine(**data))
173
  except (json.JSONDecodeError, TypeError, ValidationError):
174
  continue
@@ -246,7 +249,7 @@ def build_profile_html(p: PersonaLine):
246
  <div class="pf-header-info">
247
  <h2 class="pf-name">{_safe(p.full_name)}</h2>
248
  <p class="pf-subtitle">{_safe(p.title or "")}{("&nbsp;Β·&nbsp;" + _safe(p.company)) if p.company else ""}</p>
249
- <p class="pf-bg">{_safe(p.background)}</p>
250
  </div>
251
  </div>
252
  {('<div class="pf-meta-grid">' + "".join(meta_rows) + '</div>') if meta_rows else ''}
@@ -388,7 +391,7 @@ def _fetch_from_multi_wiki(name):
388
  continue
389
  return ""
390
 
391
- def _fetch_wikipedia_image(full_name, background=None):
392
 
393
  # 1. Wikidata (κ°€μž₯ κ°•λ ₯)
394
  img = _wikidata_image(full_name)
@@ -407,8 +410,8 @@ def _fetch_wikipedia_image(full_name, background=None):
407
  _translate_to_english_name(full_name),
408
  ]
409
 
410
- if background:
411
- queries.append(_extract_english_keywords(background))
412
 
413
  for q in queries:
414
  headers = {"User-Agent": "Mozilla/5.0"}
@@ -437,7 +440,7 @@ def generate_persona_image(name: str) -> str:
437
  return cache_path.read_text()
438
 
439
  try:
440
- data_url = _fetch_wikipedia_image(persona.full_name, persona.background)
441
  if data_url:
442
  cache_path.write_text(data_url)
443
  return data_url
@@ -692,49 +695,100 @@ def generate_persona_stream(info, endpoint):
692
 
693
  persona_ep = endpoint.rstrip("/").rsplit("/", 1)[0] + "/persona/"
694
  elapsed = _make_elapsed()
695
- q: Queue = Queue()
696
 
697
- def worker():
698
  try:
699
- r = requests.post(persona_ep, json={"info": info.strip()}, timeout=(10, 300))
700
- r.raise_for_status()
701
- q.put(("ok", r.json()))
 
 
 
 
 
 
 
 
 
 
702
  except requests.exceptions.ConnectionError:
703
- q.put(("error", f"μ—°κ²° μ‹€νŒ¨: {persona_ep}"))
704
  except requests.exceptions.Timeout:
705
- q.put(("error", "μš”μ²­ μ‹œκ°„ 초과"))
706
  except requests.RequestException as e:
707
- q.put(("error", f"μš”μ²­ μ‹€νŒ¨: {e}"))
 
 
708
 
709
- Thread(target=worker, daemon=True).start()
 
 
 
 
710
 
711
  while True:
712
  try:
713
- kind, payload = q.get_nowait(); break
 
 
 
 
714
  except Empty:
715
- yield (
716
- '<div class="ws-loading shimmer"><div class="ws-loading-title">⏳ 페λ₯΄μ†Œλ‚˜ 생성 쀑...</div>'
717
- '<div class="ws-loading-msg">AIκ°€ 인물 정보λ₯Ό κ²€μƒ‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€</div></div>',
718
- "{}", timer_text(elapsed())
719
- )
720
- time.sleep(0.3)
721
 
722
- if kind == "error":
723
- yield payload, "{}", timer_text(elapsed())
724
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
- data = payload
727
- md = "\n\n".join([
728
- f"**이름**: {data.get('name','')}",
729
- f"**λ°°κ²½**: {data.get('background','')}",
730
- f"**금육 사고 방식**: {data.get('financial_mindset','')}",
731
- f"**데이터 뢄석 방식**: {data.get('data_analysis_approach','')}",
732
- f"**λ‹΅λ³€ μŠ€νƒ€μΌ**: {data.get('response_style','')}",
733
- f"**핡심 원칙**: {', '.join(data.get('key_principles',[]))}",
734
- ])
735
- if data.get("famous_quotes"):
736
- md += f"\n\n**어둝**: {' / '.join(data['famous_quotes'])}"
737
- yield md, json.dumps(data, ensure_ascii=False, indent=2), timer_text(elapsed())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
 
739
 
740
  # ─────────────────────────────────────────────────────────────
 
126
  class PersonaLine(BaseModel):
127
  name: str
128
  full_name: str
129
+ summary: str
130
  financial_mindset: str
131
  data_analysis_approach: str
132
  response_style: str
 
169
  continue
170
  if not data.get("full_name"):
171
  data["full_name"] = data.get("name", "")
172
+ # ν˜Έν™˜μ„±: background ν•„λ“œκ°€ 있으면 summary둜 볡사
173
+ if "background" in data and "summary" not in data:
174
+ data["summary"] = data["background"]
175
  personas.append(PersonaLine(**data))
176
  except (json.JSONDecodeError, TypeError, ValidationError):
177
  continue
 
249
  <div class="pf-header-info">
250
  <h2 class="pf-name">{_safe(p.full_name)}</h2>
251
  <p class="pf-subtitle">{_safe(p.title or "")}{("&nbsp;Β·&nbsp;" + _safe(p.company)) if p.company else ""}</p>
252
+ <p class="pf-bg">{_safe(p.summary)}</p>
253
  </div>
254
  </div>
255
  {('<div class="pf-meta-grid">' + "".join(meta_rows) + '</div>') if meta_rows else ''}
 
391
  continue
392
  return ""
393
 
394
+ def _fetch_wikipedia_image(full_name, summary=None):
395
 
396
  # 1. Wikidata (κ°€μž₯ κ°•λ ₯)
397
  img = _wikidata_image(full_name)
 
410
  _translate_to_english_name(full_name),
411
  ]
412
 
413
+ if summary:
414
+ queries.append(_extract_english_keywords(summary))
415
 
416
  for q in queries:
417
  headers = {"User-Agent": "Mozilla/5.0"}
 
440
  return cache_path.read_text()
441
 
442
  try:
443
+ data_url = _fetch_wikipedia_image(persona.full_name, persona.summary)
444
  if data_url:
445
  cache_path.write_text(data_url)
446
  return data_url
 
695
 
696
  persona_ep = endpoint.rstrip("/").rsplit("/", 1)[0] + "/persona/"
697
  elapsed = _make_elapsed()
698
+ eq: Queue = Queue()
699
 
700
+ def reader():
701
  try:
702
+ body = {"info": info.strip(), "stream": True}
703
+ with requests.post(persona_ep, json=body,
704
+ headers={"Accept": "text/event-stream"},
705
+ stream=True, timeout=(10, 300)) as resp:
706
+ resp.raise_for_status()
707
+ for raw in resp.iter_lines(chunk_size=1, decode_unicode=True):
708
+ if not raw: continue
709
+ line = raw.strip()
710
+ if not line.startswith("data:"): continue
711
+ try:
712
+ eq.put(("event", json.loads(line[5:].strip())))
713
+ except json.JSONDecodeError:
714
+ continue
715
  except requests.exceptions.ConnectionError:
716
+ eq.put(("exception", f"μ—°κ²° μ‹€νŒ¨: {persona_ep}"))
717
  except requests.exceptions.Timeout:
718
+ eq.put(("exception", "μš”μ²­ μ‹œκ°„ 초과"))
719
  except requests.RequestException as e:
720
+ eq.put(("exception", f"μš”μ²­ μ‹€νŒ¨: {e}"))
721
+ finally:
722
+ eq.put(("worker_done", None))
723
 
724
+ Thread(target=reader, daemon=True).start()
725
+
726
+ log_lines = []
727
+ result_data = None
728
+ worker_done = False
729
 
730
  while True:
731
  try:
732
+ kind, payload = eq.get(timeout=0.1)
733
+ buf = [(kind, payload)]
734
+ while True:
735
+ try: buf.append(eq.get_nowait())
736
+ except Empty: break
737
  except Empty:
738
+ buf = []
 
 
 
 
 
739
 
740
+ for kind, payload in buf:
741
+ if kind == "event":
742
+ et = payload.get("type")
743
+
744
+ if et == "status":
745
+ msg = payload.get("message", "")
746
+ if msg:
747
+ log_lines.append(("status", msg))
748
+
749
+ elif et == "result":
750
+ result_data = payload
751
+ log_lines.append(("done", "페λ₯΄μ†Œλ‚˜ 생성 μ™„λ£Œ"))
752
+
753
+ elif et == "error":
754
+ msg = payload.get("message", "였λ₯˜ λ°œμƒ")
755
+ log_lines.append(("error", msg))
756
+
757
+ elif et == "done":
758
+ pass # 이미 resultμ—μ„œ 처리
759
 
760
+ elif kind == "exception":
761
+ log_lines.append(("error", str(payload)))
762
+
763
+ elif kind == "worker_done":
764
+ worker_done = True
765
+
766
+ t = timer_text(elapsed())
767
+ if result_data:
768
+ # μ΅œμ’… κ²°κ³Ό ν‘œμ‹œ
769
+ data = result_data
770
+ md = "\n\n".join([
771
+ f"**이름**: {data.get('name','')}",
772
+ f"**λ°°κ²½**: {data.get('summary','')}",
773
+ f"**금육 사고 방식**: {data.get('financial_mindset','')}",
774
+ f"**데이터 뢄석 방식**: {data.get('data_analysis_approach','')}",
775
+ f"**λ‹΅λ³€ μŠ€νƒ€μΌ**: {data.get('response_style','')}",
776
+ f"**핡심 원칙**: {', '.join(data.get('key_principles',[]))}",
777
+ ])
778
+ if data.get("famous_quotes"):
779
+ md += f"\n\n**어둝**: {' / '.join(data['famous_quotes'])}"
780
+ yield md, json.dumps(data, ensure_ascii=False, indent=2), t
781
+ break
782
+ else:
783
+ # μ§„ν–‰ 상황 ν‘œμ‹œ
784
+ panel = _wrap_log(_make_log_html(log_lines))
785
+ yield panel, "{}", t
786
+
787
+ if worker_done and not result_data:
788
+ # 였λ₯˜ λ°œμƒ μ‹œ
789
+ panel = _wrap_log(_make_log_html(log_lines))
790
+ yield panel, "{}", t
791
+ break
792
 
793
 
794
  # ─────────────────────────────────────────────────────────────
llm/generator.py CHANGED
@@ -62,7 +62,7 @@ def generate_news_info(client, user_query, intent):
62
  class Persona(BaseModel):
63
  name: str # 인물 이름
64
  full_name: str # 인물 이름
65
- background: str # 인물 λ°°κ²½ (κ²½λ ₯, μ£Όμš” 업적 λ“±)
66
  financial_mindset: str # 금육 사고 방식
67
  data_analysis_approach: str # 데이터 뢄석 방식
68
  response_style: str # μ§ˆλ¬Έμ— λŒ€ν•œ λ‹΅λ³€ μŠ€νƒ€μΌ
@@ -76,6 +76,7 @@ def generate_persona(client, user_query):
76
  "- full_name: 인물의 원문/정식 전체 이름.\n"
77
  "- name: μ‚¬μš©μžκ°€ μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ ν‘œμ‹œ 이름(ν•œκ΅­μ–΄ ν†΅μš©λͺ… μš°μ„ , μ—†μœΌλ©΄ κ°„κ²°ν•œ μ˜μ–΄).\n"
78
  "- κ΄„ν˜Έ 별칭/원문 λ³‘κΈ°λŠ” full_nameμ—λ§Œ ν¬ν•¨ν•˜κ³  nameμ—λŠ” λ„£μ§€ 말 것.\n"
 
79
  "λ‚˜λ¨Έμ§€ ν•„λ“œλŠ” 사싀 기반으둜 μΆ©μ‹€νžˆ μž‘μ„±ν•˜μ‹œμ˜€."
80
  )
81
 
@@ -123,7 +124,7 @@ def build_full_prompt(user_query, context, intent, persona=None):
123
 
124
  [μ„ νƒλœ 페λ₯΄μ†Œλ‚˜]
125
  이름: {persona.name}
126
- λ°°κ²½: {persona.background}
127
  금육 사고방식: {persona.financial_mindset}
128
  데이터 뢄석 방식: {persona.data_analysis_approach}
129
  λ‹΅λ³€ μŠ€νƒ€μΌ: {persona.response_style}
 
62
  class Persona(BaseModel):
63
  name: str # 인물 이름
64
  full_name: str # 인물 이름
65
+ summary: str # 인물 μš”μ•½ (κ°„λ‹¨ν•œ μ†Œκ°œ)
66
  financial_mindset: str # 금육 사고 방식
67
  data_analysis_approach: str # 데이터 뢄석 방식
68
  response_style: str # μ§ˆλ¬Έμ— λŒ€ν•œ λ‹΅λ³€ μŠ€νƒ€μΌ
 
76
  "- full_name: 인물의 원문/정식 전체 이름.\n"
77
  "- name: μ‚¬μš©μžκ°€ μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ ν‘œμ‹œ 이름(ν•œκ΅­μ–΄ ν†΅μš©λͺ… μš°μ„ , μ—†μœΌλ©΄ κ°„κ²°ν•œ μ˜μ–΄).\n"
78
  "- κ΄„ν˜Έ 별칭/원문 λ³‘κΈ°λŠ” full_nameμ—λ§Œ ν¬ν•¨ν•˜κ³  nameμ—λŠ” λ„£μ§€ 말 것.\n"
79
+ "- summary: 인물의 κ°„λ‹¨ν•œ μ†Œκ°œ μš”μ•½ (2-3λ¬Έμž₯ μ •λ„λ‘œ κ°„κ²°ν•˜κ²Œ).\n"
80
  "λ‚˜λ¨Έμ§€ ν•„λ“œλŠ” 사싀 기반으둜 μΆ©μ‹€νžˆ μž‘μ„±ν•˜μ‹œμ˜€."
81
  )
82
 
 
124
 
125
  [μ„ νƒλœ 페λ₯΄μ†Œλ‚˜]
126
  이름: {persona.name}
127
+ μš”μ•½: {persona.summary}
128
  금육 사고방식: {persona.financial_mindset}
129
  데이터 뢄석 방식: {persona.data_analysis_approach}
130
  λ‹΅λ³€ μŠ€νƒ€μΌ: {persona.response_style}
persona/make_persona.py CHANGED
@@ -68,13 +68,13 @@ def save_persona_jsonl(persona, query, file_name=None):
68
  def print_persona(persona):
69
  print("\n[Persona 생성 κ²°κ³Ό]")
70
  print(f"- 이름: {persona.name}")
71
- print(f"- λ°°κ²½: {persona.background}")
72
  print(f"- 금육 사고 방식: {persona.financial_mindset}")
73
  print(f"- 데이터 뢄석 방식: {persona.data_analysis_approach}")
74
  print(f"- λ‹΅λ³€ μŠ€νƒ€μΌ: {persona.response_style}")
75
- print(f"- 핡심 원칙: {', '.join(persona.key_principles)}")
76
  if getattr(persona, "famous_quotes", None):
77
- print(f"- 어둝: {', '.join(persona.famous_quotes)}")
78
 
79
 
80
  def make_persona(info):
 
68
  def print_persona(persona):
69
  print("\n[Persona 생성 κ²°κ³Ό]")
70
  print(f"- 이름: {persona.name}")
71
+ print(f"- μš”μ•½: {persona.summary}")
72
  print(f"- 금육 사고 방식: {persona.financial_mindset}")
73
  print(f"- 데이터 뢄석 방식: {persona.data_analysis_approach}")
74
  print(f"- λ‹΅λ³€ μŠ€νƒ€μΌ: {persona.response_style}")
75
+ print(f"- 핡심 원칙: {', '.join(persona.key_principles)}", flush=True)
76
  if getattr(persona, "famous_quotes", None):
77
+ print(f"- 어둝: {', '.join(persona.famous_quotes)}", flush=True)
78
 
79
 
80
  def make_persona(info):
persona/persona_loader.py CHANGED
@@ -13,6 +13,10 @@ def load_personas():
13
  line = line.strip()
14
  if line:
15
  data = json.loads(line)
 
 
 
 
16
  # full_name μ—†μœΌλ©΄ name μ‚¬μš©ν•˜μ—¬ 채움
17
  if isinstance(data, dict) and not data.get("full_name"):
18
  data["full_name"] = data.get("name", "")
 
13
  line = line.strip()
14
  if line:
15
  data = json.loads(line)
16
+ # ν˜Έν™˜μ„±: background ν•„λ“œκ°€ 있으면 summary둜 λ§€ν•‘
17
+ if isinstance(data, dict) and "background" in data and "summary" not in data:
18
+ data["summary"] = data["background"]
19
+ # background ν•„λ“œλŠ” μ œκ±°ν•˜μ§€ μ•Šκ³  μœ μ§€ (ν˜Έν™˜μ„±)
20
  # full_name μ—†μœΌλ©΄ name μ‚¬μš©ν•˜μ—¬ 채움
21
  if isinstance(data, dict) and not data.get("full_name"):
22
  data["full_name"] = data.get("name", "")