feat: implement conversational hybrid RAG fallback routing and optimize presentation layout
Browse files- AGENTS.md +4 -4
- README.md +7 -0
- app.py +42 -593
- src/retrieval/finRetrieval.py +139 -10
- src/utils/ui_templates.py +17 -5
- tests/test_retrieval.py +27 -0
AGENTS.md
CHANGED
|
@@ -122,10 +122,10 @@ def test_4_core_scenarios():
|
|
| 122 |
- [x] **4. ์ ์ /๋์ ๋ฐฉ์ด ํ
์คํธ**: Ruff/Mypy ํต๊ณผ, `python -c "import app"` ์ ์ ๋น๋, `smoke_test_rag.py` ์ฑ๊ณต ๊ฒ์ฆ
|
| 123 |
|
| 124 |
## ๊ฐ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ (Gradio UI/UX ๋ํ
์ผ ๊ฐ์ ๋จ๊ณ)
|
| 125 |
-
- [
|
| 126 |
-
- [
|
| 127 |
-
- [
|
| 128 |
-
- [
|
| 129 |
|
| 130 |
|
| 131 |
|
|
|
|
| 122 |
- [x] **4. ์ ์ /๋์ ๋ฐฉ์ด ํ
์คํธ**: Ruff/Mypy ํต๊ณผ, `python -c "import app"` ์ ์ ๋น๋, `smoke_test_rag.py` ์ฑ๊ณต ๊ฒ์ฆ
|
| 123 |
|
| 124 |
## ๊ฐ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ (Gradio UI/UX ๋ํ
์ผ ๊ฐ์ ๋จ๊ณ)
|
| 125 |
+
- [x] **1. ํ๋ฉด ๋๋น ๋ํญ ํ๋**: `.gradio-container` ๋ฐ ๋ธ๋ก ๋ ์ด์์์ max-width๋ฅผ ๋ํญ ํ์ฅํ์ฌ ๋ํ๋ฉด ์ง์
|
| 126 |
+
- [x] **2. ์์ ์ง๋ฌธ ์ต์๋จ(์ฑ๋ด ์) ์ด๋**: CSS Flexbox order ๋๋ Blocks ๊ตฌ์กฐ ๊ฐํธ์ ํตํด ์์ ์ง๋ฌธ์ ํ๋ฉด ๋งจ ์๋ก ๊ณ ์
|
| 127 |
+
- [x] **3. ๋ฒํผ ํ
๋๋ฆฌ ์๊ฒ ๊ฐ์ **: ์์ ์ง๋ฌธ ๋ฒํผ์ ํฌ์ธํธ ๋ณด๋ ๋๊ป๋ฅผ ์ถ์ํ๊ณ ์๊ณ ๊น๋ํ๊ฒ ๋ฏธ๋๋ฉ๋ฆฌ์ฆ ๋์์ธ ์ ์ฉ
|
| 128 |
+
- [x] **4. ์ ์ /๋์ ๊ฒ์ฆ**: Ruff/Mypy ํต๊ณผ ๋ฐ `browser_subagent`๋ฅผ ํตํ ์ค์ ๋ ๋๋ง ๋ฌด๊ฒฐ์ฑ ์คํฌ๋ฆฐ์ท ๊ฒ์ฆ
|
| 129 |
|
| 130 |
|
| 131 |
|
README.md
CHANGED
|
@@ -73,6 +73,13 @@ pinned: false
|
|
| 73 |
- `Vector Retriever`: ๋ณธ๋ฌธ ์ฒญํฌ ์๋ฏธ ์ ์ฌ๋ ๊ฒ์
|
| 74 |
- `VectorCypher Retriever`: ๋ฒกํฐ ๊ฒ์ ํ ํด๋น ๊ธฐ์ฌ์ ์ฐ๊ด ๊ทธ๋ํ(๊ธฐ์
ยท๊ธฐ์ ยท์๋น์ค) ๋ฐํ (ํธ๋ ๋ ๋ถ์์ ์ต์ ํ)
|
| 75 |
- `Text2Cypher Retriever`: ์์ฐ์ด โ Cypher ์ฟผ๋ฆฌ ์๋ ๋ณํ ๋ฐ ๋ฐ์ดํฐ ์ง๊ณ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
---
|
| 78 |
|
|
|
|
| 73 |
- `Vector Retriever`: ๋ณธ๋ฌธ ์ฒญํฌ ์๋ฏธ ์ ์ฌ๋ ๊ฒ์
|
| 74 |
- `VectorCypher Retriever`: ๋ฒกํฐ ๊ฒ์ ํ ํด๋น ๊ธฐ์ฌ์ ์ฐ๊ด ๊ทธ๋ํ(๊ธฐ์
ยท๊ธฐ์ ยท์๋น์ค) ๋ฐํ (ํธ๋ ๋ ๋ถ์์ ์ต์ ํ)
|
| 75 |
- `Text2Cypher Retriever`: ์์ฐ์ด โ Cypher ์ฟผ๋ฆฌ ์๋ ๋ณํ ๋ฐ ๋ฐ์ดํฐ ์ง๊ณ
|
| 76 |
+
- **๋ํ ๋งฅ๋ฝ ๋ฐ์ ํ์ด๋ธ๋ฆฌ๋ RAG & ์ง๋ฅํ Fallback ๋ผ์ฐํ
**:
|
| 77 |
+
- Neo4j์์ ๊ฒ์๋ ์ง์ ๊ทธ๋ํ ์ ๋ณด์ ์ฌ์ฉ์์ ์ง๋ฌธ, ๊ทธ๋ฆฌ๊ณ **์ต๊ทผ ๋ํ ํ์คํ ๋ฆฌ(์ต๊ทผ 3๊ฐ ๋ฉ์์ง)**๋ฅผ ์ข
ํฉ ๋ถ์ํ์ฌ GPT-4o ๊ธฐ๋ฐ ์๊ฐ ํ์ ๊ฐ๋๋ ์ผ(`_is_context_sufficient`)์ ์ค์๊ฐ ๊ตฌ๋.
|
| 78 |
+
- ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ง๋ฌธ์ ๋ต๋ณํ๊ธฐ ์ถฉ๋ถํ ๊ฒฝ์ฐ `GraphRAG` ๋ชจ๋๋ก ๊ตฌ๋ํ์ฌ ๊ตฌ์ฒด์ ์ธ ๊ธฐ์ฌ ์ถ์ฒ ๋งํฌ(URL, ์ ๋ชฉ ๋ฑ)๋ฅผ ํฌํจํ ํฉํธ ๊ธฐ๋ฐ ๋ต๋ณ ์ ๊ณต.
|
| 79 |
+
- ๊ด๋ จ ์ ๋ณด๊ฐ ๋ถ์กฑํ๊ฑฐ๋ ๊ธ์ต/IT ๋ด์ค๋ฅผ ๋ฒ์ด๋ ์ผ๋ฐ ์ง๋ฌธ(์: ์ํ ๊ณต์, ์ผ์ ๋ํ ๋ฑ), ํน์ ์ด์ ๋ํ ๋งฅ๋ฝ์ ์์กดํ๋ ์ง๋ฌธ์ ๋ํด์๋ ํ์คํ ๋ฆฌ๋ฅผ ์ข
ํฉ ๋ถ์ํ์ฌ ์ผ๋ฐ ์ง์ ๋ต๋ณ(`general` ๋ชจ๋)์ผ๋ก ์ ์ฐํ๊ฒ ์ค์์นญํ์ฌ ํ๊ฐ(Hallucination) ๋ฐฉ์ง ๋ฐ ์์ ์ฑ ๊ทน๋ํ.
|
| 80 |
+
- **์ฌ์ฉ์ ๊ฒฝํ(UX) ๋ฐ ํ๋ฆฌ๋ฏธ์ UI ์ต์ ํ**:
|
| 81 |
+
- **์ฌ๋ฆผ ์ฑ ๋ฒ๋ธ**: Gradio ๋งํฌ๋ค์ด ๋ ๋๋ฌ๊ฐ ๋ด๋ถ `<p>`, `<li>` ํ๊ทธ ๋ฑ์ ๋ถ์ฌํ๋ ๋น์ ์์ ์ผ๋ก ํฐ ์ํ ์ฌ๋ฐฑ๊ณผ ๋ง์ง์ ์ถ์(`.message` ํจ๋ฉ `10px 14px` ์กฐ์ ๋ฐ ๋ด๋ถ ๋ง์ง ์ต์ ํ)ํ์ฌ ๊ฐ๋
์ฑ ๋๊ณ ์ฌ๋ฆผํ ํ๋ฆฌ๋ฏธ์ ๋งํ์ UI ๊ตฌํ.
|
| 82 |
+
- **์ค์๊ฐ ํ์ ์งํ์ํฉ ์๊ฐํ**: LangGraph ๋ํ ์คํธ๋ฆผ(Stream) ์ฐ๋์ ํตํด `"๐ ๊ฒ์ ์งํ ์ค..."`, `"๐ก ๋ต๋ณ ์์ฑ ์ค..."` ๊ณผ์ ์ ์ค์๊ฐ์ผ๋ก ๋
ธ์ถํ์ฌ ๋ค๋จ๊ณ RAG์ ์ง์ฐ ์๊ฐ ๋์์ ์ฌ์ฉ์ ์ฒด๊ฐ ๋๊ธฐ ์ฑ๋ฅ ํ์ .
|
| 83 |
|
| 84 |
---
|
| 85 |
|
app.py
CHANGED
|
@@ -14,7 +14,8 @@ import dotenv
|
|
| 14 |
import gradio as gr
|
| 15 |
from langgraph.graph import END, StateGraph
|
| 16 |
|
| 17 |
-
from src.retrieval.finRetrieval import graphrag
|
|
|
|
| 18 |
|
| 19 |
dotenv.load_dotenv()
|
| 20 |
|
|
@@ -44,8 +45,9 @@ except Exception as e:
|
|
| 44 |
class ChatState(TypedDict):
|
| 45 |
question: str # ์ฌ์ฉ์ ์ง๋ฌธ
|
| 46 |
history: List[dict] # ๋ํ ํ์คํ ๋ฆฌ [{"role": "user"/"assistant", "content": "..."}]
|
| 47 |
-
context: str # GraphRAG ๊ฒ์ ๊ฒฐ๊ณผ
|
| 48 |
-
answer: str
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -54,16 +56,33 @@ class ChatState(TypedDict):
|
|
| 54 |
|
| 55 |
|
| 56 |
def retrieve_node(state: ChatState) -> ChatState:
|
| 57 |
-
"""Node 1:
|
| 58 |
try:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
sources = []
|
| 64 |
-
seen_urls = set()
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
meta = getattr(item, "metadata", {})
|
| 68 |
title = meta.get("article_title")
|
| 69 |
url = meta.get("article_url")
|
|
@@ -142,17 +161,15 @@ def retrieve_node(state: ChatState) -> ChatState:
|
|
| 142 |
|
| 143 |
except Exception as e:
|
| 144 |
context = f"[๊ฒ์ ์ค๋ฅ: {e}]"
|
| 145 |
-
return {**state, "context": context}
|
| 146 |
|
| 147 |
|
| 148 |
def generate_node(state: ChatState) -> ChatState:
|
| 149 |
"""Node 2: ๋ํ ํ์คํ ๋ฆฌ๋ฅผ ๊ณ ๋ คํ์ฌ ์ต์ข
๋ต๋ณ ์์ฑ
|
| 150 |
|
| 151 |
-
GraphRAG
|
| 152 |
-
|
| 153 |
"""
|
| 154 |
-
# GraphRAG ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ก ๋ต๋ณ์ผ๋ก ์ฌ์ฉ
|
| 155 |
-
# (ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ํ์ ์ง๋ฌธ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ฉด ์ด ๋
ธ๋๋ฅผ ํ์ฅํ์ธ์)
|
| 156 |
answer = state["context"] if state["context"] else "๊ด๋ จ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."
|
| 157 |
return {**state, "answer": answer}
|
| 158 |
|
|
@@ -197,6 +214,7 @@ def chat(message: str, history: list):
|
|
| 197 |
"history": history,
|
| 198 |
"context": "",
|
| 199 |
"answer": "",
|
|
|
|
| 200 |
}
|
| 201 |
|
| 202 |
yield "๐ ์ค์๊ฐ ์ง์ ๊ทธ๋ํ์์ ๊ด๋ จ ๋ด์ค๋ฅผ ๊ฒ์ํ๋ ์ค์
๋๋ค..."
|
|
@@ -205,7 +223,11 @@ def chat(message: str, history: list):
|
|
| 205 |
# LangGraph์ stream์ ์ฌ์ฉํ์ฌ ๊ฐ ๋
ธ๋ ์คํ ์์ ๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์
|
| 206 |
for event in chat_graph.stream(state):
|
| 207 |
if "retrieve" in event:
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
elif "generate" in event:
|
| 210 |
yield event["generate"]["answer"]
|
| 211 |
except Exception as e:
|
|
@@ -273,97 +295,6 @@ def get_db_stats() -> Dict[str, Any]:
|
|
| 273 |
return stats
|
| 274 |
|
| 275 |
|
| 276 |
-
def build_stats_html(stats: Dict[str, Any]) -> str:
|
| 277 |
-
"""์กฐํ๋ ์ง์ ๊ทธ๋ํ ํต๊ณ ์ ๋ณด๋ค์ ๋ฐํ์ผ๋ก ๋ฏธ๋ คํ๊ณ ์ปดํฉํธํ ๋์๋ณด๋์ฉ HTML์ ์์ฑํฉ๋๋ค."""
|
| 278 |
-
# 1. ์ต์ ๋ด์ค ํค์๋ ๋ฐฐ์ง HTML ์์ฑ (๋ฅ๊ทผ ๋ค๋ชจ ํํ)
|
| 279 |
-
keyword_html: str = ""
|
| 280 |
-
for t in stats.get("techs_list", []):
|
| 281 |
-
keyword_html += f"""
|
| 282 |
-
<span class="keyword-badge"># {t['name']}</span>
|
| 283 |
-
"""
|
| 284 |
-
if not keyword_html:
|
| 285 |
-
keyword_html = '<div style="font-size:12px; color:#94a3b8;">๋ฑ๋ก๋ ํค์๋๊ฐ ์์ต๋๋ค.</div>'
|
| 286 |
-
|
| 287 |
-
# 1.5. ์ต์ ์ฃผ๋ชฉ ๊ธฐ์
๋ฐฐ์ง HTML ์์ฑ (ํค์๋ ๋ฐฐ์ง์ ๋์ผ ์คํ์ผ๋ก ํต์ผ)
|
| 288 |
-
company_html: str = ""
|
| 289 |
-
for c in stats.get("companies_list", []):
|
| 290 |
-
company_html += f"""
|
| 291 |
-
<span class="keyword-badge">๐ข {c['name']}</span>
|
| 292 |
-
"""
|
| 293 |
-
if not company_html:
|
| 294 |
-
company_html = '<div style="font-size:12px; color:#94a3b8;">๋ฑ๋ก๋ ๊ธฐ์
์ด ์์ต๋๋ค.</div>'
|
| 295 |
-
|
| 296 |
-
# 2. ์ต๊ทผ ๊ธฐ์ฌ ๋ฆฌ์คํธ HTML ์์ฑ (์ต๋ 4๊ฐ) - ์ ์ฒด ์์ญ ํด๋ฆญ ์ ์ด๋ํ๋๋ก a ํ๊ทธ๋ก ๋ํ
|
| 297 |
-
news_list_html: str = ""
|
| 298 |
-
for a in stats.get("recent_articles", []):
|
| 299 |
-
title = a["title"]
|
| 300 |
-
url = a["url"] if a["url"] and str(a["url"]).lower() != "nan" else "#"
|
| 301 |
-
target = 'target="_blank"' if url != "#" else ""
|
| 302 |
-
date_str = str(a['date'])[:10] if a['date'] else ""
|
| 303 |
-
news_list_html += f"""
|
| 304 |
-
<a class="news-item-link" href="{url}" {target}>
|
| 305 |
-
<div class="news-item">
|
| 306 |
-
<div class="news-title">{title}</div>
|
| 307 |
-
<div class="news-meta">๐๏ธ {date_str}</div>
|
| 308 |
-
</div>
|
| 309 |
-
</a>
|
| 310 |
-
"""
|
| 311 |
-
if not news_list_html:
|
| 312 |
-
news_list_html = '<div style="font-size:12px; color:#94a3b8;">์ต๊ทผ ์์ง๋ ๊ธฐ์ฌ๊ฐ ์์ต๋๋ค.</div>'
|
| 313 |
-
|
| 314 |
-
# node_count = stats['companies'] + stats['technologies']
|
| 315 |
-
|
| 316 |
-
html: str = f"""
|
| 317 |
-
<div class="dashboard-container">
|
| 318 |
-
<!-- Ambient background elements for beautiful glass effects -->
|
| 319 |
-
<div class="ambient-glow"></div>
|
| 320 |
-
|
| 321 |
-
<div style="font-size: 16px; font-weight: 850; color: #334155; margin-bottom: 2px; display: flex; align-items: center; gap: 6px; letter-spacing: -0.02em;">
|
| 322 |
-
๐ <span>FinGraph AI Terminal</span>
|
| 323 |
-
</div>
|
| 324 |
-
<p style="font-size: 11px; color: #475569; margin-top: -2px; margin-bottom: 12px; font-weight: 600;">GraphRAG ์ค์๊ฐ ๋ถ์ ์์ง ์ํ</p>
|
| 325 |
-
|
| 326 |
-
<!-- ์ค์๊ฐ ์์ง ํ
๋ ๋ฉํธ๋ฆฌ (4๊ฐ ๋ฉํธ๋ฆญ ํตํํฉ) -->
|
| 327 |
-
<div class="stats-grid">
|
| 328 |
-
<div class="stat-card">
|
| 329 |
-
<div class="stat-lbl">๐ก ๋ถ์ ๋ชจ๋ธ</div>
|
| 330 |
-
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">GPT-4o</div>
|
| 331 |
-
</div>
|
| 332 |
-
<div class="stat-card">
|
| 333 |
-
<div class="stat-lbl">๐ข ๋์ ํ์ฌ</div>
|
| 334 |
-
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['companies']}๊ฐ</div>
|
| 335 |
-
</div>
|
| 336 |
-
<div class="stat-card">
|
| 337 |
-
<div class="stat-lbl">๐ ๋ด์ค ํค์๋</div>
|
| 338 |
-
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['technologies']}๊ฐ</div>
|
| 339 |
-
</div>
|
| 340 |
-
<div class="stat-card">
|
| 341 |
-
<div class="stat-lbl">๐ฐ ๋ถ์์ฉ ๋ด์ค ๊ธฐ์ฌ</div>
|
| 342 |
-
<div class="stat-val" style="font-size: 13px !important; font-weight: 800 !important; color: #334155;">{stats['articles']}๊ฑด</div>
|
| 343 |
-
</div>
|
| 344 |
-
</div>
|
| 345 |
-
|
| 346 |
-
<div class="section-subtitle" style="color: #334155;">๐ก ์ต์ ๋ด์ค ํค์๋</div>
|
| 347 |
-
<div class="keyword-container">
|
| 348 |
-
{keyword_html}
|
| 349 |
-
</div>
|
| 350 |
-
|
| 351 |
-
<div class="section-subtitle" style="color: #334155;">๐ข ์ต์ ์ฃผ๋ชฉ ๊ธฐ์
</div>
|
| 352 |
-
<div class="keyword-container">
|
| 353 |
-
{company_html}
|
| 354 |
-
</div>
|
| 355 |
-
|
| 356 |
-
<div class="section-subtitle" style="color: #334155;">๐ฐ ์ต์ ๋ด์ค ํผ๋</div>
|
| 357 |
-
<div class="news-feed-container">
|
| 358 |
-
<div class="news-feed">
|
| 359 |
-
{news_list_html}
|
| 360 |
-
</div>
|
| 361 |
-
</div>
|
| 362 |
-
</div>
|
| 363 |
-
"""
|
| 364 |
-
return html
|
| 365 |
-
|
| 366 |
-
|
| 367 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 368 |
# 5. Gradio UI ๊ตฌ์ฑ
|
| 369 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -380,488 +311,6 @@ theme_obj = gr.themes.Soft(
|
|
| 380 |
secondary_hue="slate",
|
| 381 |
)
|
| 382 |
|
| 383 |
-
custom_css: str = """
|
| 384 |
-
body {
|
| 385 |
-
background-color: #fbf9f6;
|
| 386 |
-
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
| 387 |
-
color: #0f172a !important; /* ๊ธฐ๋ณธ ๊ฒ์ ๏ฟฝ๏ฟฝ๏ฟฝ */
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
/* Ambient glow point backgrounds (๋ณด๋ผ์ ์์ฒ ๋ฐฐ์ , ์์ํ ์ค์นด์ด๋ธ๋ฃจ์ ํ
์ผ๊ทธ๋ฆฐ ํค) */
|
| 391 |
-
.ambient-glow {
|
| 392 |
-
position: fixed;
|
| 393 |
-
top: 0; left: 0; right: 0; bottom: 0;
|
| 394 |
-
background: radial-gradient(circle at 85% 15%, rgba(14, 165, 233, 0.06) 0%, transparent 45%),
|
| 395 |
-
radial-gradient(circle at 15% 85%, rgba(20, 184, 166, 0.06) 0%, transparent 45%);
|
| 396 |
-
z-index: -1;
|
| 397 |
-
pointer-events: none;
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
/* ๋์๋ณด๋ ํฌ๋ช
๊ธ๋์ค๋ชจํผ์ฆ ์ปจํ
์ด๋ */
|
| 401 |
-
.dashboard-container {
|
| 402 |
-
background: rgba(255, 255, 255, 0.8) !important;
|
| 403 |
-
backdrop-filter: blur(24px) !important;
|
| 404 |
-
-webkit-backdrop-filter: blur(24px) !important;
|
| 405 |
-
border: 1px solid #cbd5e1 !important; /* ๊น๋ํ ๋ดํธ๋ด ์ฌ๋ ์ดํธ ํ
๋๋ฆฌ */
|
| 406 |
-
border-radius: 12px;
|
| 407 |
-
padding: 16px;
|
| 408 |
-
box-shadow: 0 4px 12px -2px rgba(15, 23, 42, 0.03) !important;
|
| 409 |
-
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 410 |
-
}
|
| 411 |
-
.dark .dashboard-container {
|
| 412 |
-
background: rgba(15, 23, 42, 0.55) !important;
|
| 413 |
-
border-color: rgba(14, 165, 233, 0.25) !important;
|
| 414 |
-
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.3) !important;
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
/* ํต๊ณ ๊ทธ๋ฆฌ๋ ๋ฐ ๊ธ๋์ค ์นด๋ */
|
| 418 |
-
.stats-grid {
|
| 419 |
-
display: grid;
|
| 420 |
-
grid-template-columns: repeat(2, 1fr);
|
| 421 |
-
gap: 10px;
|
| 422 |
-
margin-bottom: 15px;
|
| 423 |
-
}
|
| 424 |
-
.stat-card {
|
| 425 |
-
background: rgba(255, 255, 255, 0.95);
|
| 426 |
-
border: 1px solid #cbd5e1;
|
| 427 |
-
border-radius: 8px;
|
| 428 |
-
padding: 10px;
|
| 429 |
-
text-align: center;
|
| 430 |
-
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.01);
|
| 431 |
-
transition: all 0.25s ease-in-out;
|
| 432 |
-
}
|
| 433 |
-
.stat-card:hover {
|
| 434 |
-
transform: translateY(-2px);
|
| 435 |
-
background: rgba(255, 255, 255, 1);
|
| 436 |
-
border-color: #0ea5e9; /* ํธ๋ฒ ์ ์ค์นด์ด ๋ธ๋ฃจ */
|
| 437 |
-
box-shadow: 0 4px 12px -2px rgba(14, 165, 233, 0.1);
|
| 438 |
-
}
|
| 439 |
-
.dark .stat-card {
|
| 440 |
-
background: rgba(30, 41, 59, 0.7);
|
| 441 |
-
border-color: rgba(14, 165, 233, 0.2);
|
| 442 |
-
color: #f1f5f9;
|
| 443 |
-
}
|
| 444 |
-
.dark .stat-card:hover {
|
| 445 |
-
border-color: #38bdf8;
|
| 446 |
-
}
|
| 447 |
-
.stat-val {
|
| 448 |
-
font-size: 16px !important;
|
| 449 |
-
font-weight: 850 !important;
|
| 450 |
-
color: #0f172a !important; /* ํ์คํ ๊ณ ๋๋น ๊ฒ์ ์ ๊ธ์จ */
|
| 451 |
-
margin-top: 2px;
|
| 452 |
-
}
|
| 453 |
-
.dark .stat-val {
|
| 454 |
-
color: #f8fafc !important;
|
| 455 |
-
}
|
| 456 |
-
.stat-lbl {
|
| 457 |
-
font-size: 11px !important;
|
| 458 |
-
color: #334155;
|
| 459 |
-
font-weight: 600;
|
| 460 |
-
}
|
| 461 |
-
.dark .stat-lbl {
|
| 462 |
-
color: #94a3b8;
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
/* ์ต์ ๋ด์ค ํค์๋ ์ปจํ
์ด๋ ๋ฐ ๋ฅ๊ทผ ๋ฐฐ์ง ์คํ์ผ (๋ณด๋ผ์ ๋ฐฐ์ , ์ฌ๋ ์ดํธ ๋ฐ ๊ฒ์ ์ ๊ธ์จ) */
|
| 466 |
-
.keyword-container {
|
| 467 |
-
display: flex;
|
| 468 |
-
flex-wrap: wrap;
|
| 469 |
-
gap: 8px;
|
| 470 |
-
margin-bottom: 12px;
|
| 471 |
-
}
|
| 472 |
-
.keyword-badge {
|
| 473 |
-
display: inline-block;
|
| 474 |
-
background: #f1f5f9 !important; /* ์ฐํ ๋ดํธ๋ด ์ฌ๋ ์ดํธ */
|
| 475 |
-
border: 1px solid #cbd5e1 !important; /* ์ฌ๋ ์ดํธ ํ
๋๋ฆฌ */
|
| 476 |
-
border-radius: 8px !important;
|
| 477 |
-
padding: 6px 12px;
|
| 478 |
-
font-size: 11px !important;
|
| 479 |
-
font-weight: 700;
|
| 480 |
-
color: #0f172a !important; /* ํ์คํ ๊ฒ์ ์ */
|
| 481 |
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) !important;
|
| 482 |
-
transition: all 0.2s ease-in-out;
|
| 483 |
-
}
|
| 484 |
-
.keyword-badge:hover {
|
| 485 |
-
background: #e2e8f0 !important;
|
| 486 |
-
transform: scale(1.03);
|
| 487 |
-
}
|
| 488 |
-
.dark .keyword-badge {
|
| 489 |
-
background: rgba(15, 23, 42, 0.4) !important;
|
| 490 |
-
border-color: rgba(14, 165, 233, 0.25) !important;
|
| 491 |
-
color: #cbd5e1 !important;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/* ์ต๊ทผ ๋ด์ค ํผ๋ ํด๋ฆญ ๊ฐ๋ฅํ ์นด๋ ๋ ์ด์์ */
|
| 495 |
-
.news-feed-container {
|
| 496 |
-
max-height: 350px;
|
| 497 |
-
overflow-y: auto;
|
| 498 |
-
border: 1px solid #cbd5e1;
|
| 499 |
-
border-radius: 6px;
|
| 500 |
-
padding: 8px;
|
| 501 |
-
background: rgba(255, 255, 255, 0.7);
|
| 502 |
-
}
|
| 503 |
-
.dark .news-feed-container {
|
| 504 |
-
background: rgba(30, 41, 59, 0.5);
|
| 505 |
-
border-color: rgba(14, 165, 233, 0.15);
|
| 506 |
-
}
|
| 507 |
-
/* ์คํฌ๋กค๋ฐ ์ปค์คํ
*/
|
| 508 |
-
.news-feed-container::-webkit-scrollbar {
|
| 509 |
-
width: 4px;
|
| 510 |
-
}
|
| 511 |
-
.news-feed-container::-webkit-scrollbar-track {
|
| 512 |
-
background: transparent;
|
| 513 |
-
}
|
| 514 |
-
.news-feed-container::-webkit-scrollbar-thumb {
|
| 515 |
-
background: rgba(148, 163, 184, 0.4);
|
| 516 |
-
border-radius: 2px;
|
| 517 |
-
}
|
| 518 |
-
.dark .news-feed-container::-webkit-scrollbar-thumb {
|
| 519 |
-
background: rgba(148, 163, 184, 0.2);
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
.news-item-link {
|
| 523 |
-
text-decoration: none;
|
| 524 |
-
display: block;
|
| 525 |
-
margin-bottom: 8px;
|
| 526 |
-
}
|
| 527 |
-
.news-item-link:last-child {
|
| 528 |
-
margin-bottom: 0;
|
| 529 |
-
}
|
| 530 |
-
.news-item {
|
| 531 |
-
border-left: 3px solid #0ea5e9; /* ํ๋์ ์ค์
๋ธ๋ฃจ ํฌ์ธํธ */
|
| 532 |
-
padding: 8px 10px;
|
| 533 |
-
background: rgba(255, 255, 255, 0.8);
|
| 534 |
-
border-radius: 0 6px 6px 0;
|
| 535 |
-
transition: all 0.2s ease-in-out;
|
| 536 |
-
cursor: pointer;
|
| 537 |
-
}
|
| 538 |
-
.news-item-link:hover .news-item {
|
| 539 |
-
background: rgba(255, 255, 255, 1);
|
| 540 |
-
border-left-color: #0284c7;
|
| 541 |
-
transform: translateX(3px);
|
| 542 |
-
box-shadow: 0 2px 6px rgba(14, 165, 233, 0.08);
|
| 543 |
-
}
|
| 544 |
-
.dark .news-item {
|
| 545 |
-
background: rgba(30, 41, 59, 0.3);
|
| 546 |
-
}
|
| 547 |
-
.dark .news-item-link:hover .news-item {
|
| 548 |
-
background: rgba(30, 41, 59, 0.65);
|
| 549 |
-
border-left-color: #38bdf8;
|
| 550 |
-
}
|
| 551 |
-
.news-title {
|
| 552 |
-
font-size: 12px !important;
|
| 553 |
-
font-weight: 600;
|
| 554 |
-
color: #1b1c1a;
|
| 555 |
-
line-height: 1.4;
|
| 556 |
-
white-space: nowrap;
|
| 557 |
-
overflow: hidden;
|
| 558 |
-
text-overflow: ellipsis;
|
| 559 |
-
}
|
| 560 |
-
.dark .news-title {
|
| 561 |
-
color: #cbd5e1;
|
| 562 |
-
}
|
| 563 |
-
.news-meta {
|
| 564 |
-
font-size: 10px !important;
|
| 565 |
-
color: #94a3b8;
|
| 566 |
-
margin-top: 2px;
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
/* ์๋ธํ์ดํ ํค๋ ์คํ์ผ (์ฐํ ์๋ฉ๋๋/ํ
์ผ ๋ฐฐ๊ฒฝ + ๋ฅ ํ
์ผ ๊ธ์จ) */
|
| 570 |
-
.section-subtitle {
|
| 571 |
-
font-size: 13px !important;
|
| 572 |
-
font-weight: 800;
|
| 573 |
-
color: #0f766e !important; /* ๋ฅ ํ
์ผ ๊ธ์จ */
|
| 574 |
-
background: rgba(20, 184, 166, 0.1) !important; /* ์ฐํ ํ
์ผ ๊ธ๋์ค ๋ฐฐ๊ฒฝ */
|
| 575 |
-
margin: 18px 0 8px 0;
|
| 576 |
-
padding: 6px 10px !important;
|
| 577 |
-
border-radius: 6px !important;
|
| 578 |
-
border-left: 3px solid #14b8a6 !important; /* ์ ๋ช
ํ ํ
์ผ ์ธ๋ก์ */
|
| 579 |
-
display: flex;
|
| 580 |
-
align-items: center;
|
| 581 |
-
gap: 6px;
|
| 582 |
-
}
|
| 583 |
-
.dark .section-subtitle {
|
| 584 |
-
color: #99f6e4 !important;
|
| 585 |
-
background: rgba(20, 184, 166, 0.2) !important;
|
| 586 |
-
border-left-color: #2dd4bf !important;
|
| 587 |
-
}
|
| 588 |
-
|
| 589 |
-
/* โโ ์ฐ์ปด ๋ณด๋(์๊ฐ๊ธ ์นด๋) ์๋จ ์งค๋ฆผ ๋ฐฉ์ด ๋ฐ ๊ฒฐํฉ ์ค๋น โโ */
|
| 590 |
-
.placeholder, [class*="placeholder"] {
|
| 591 |
-
justify-content: flex-start !important;
|
| 592 |
-
padding-top: 15px !important; /* ์๋จ ์งค๋ฆผ ์์ ๋ฐฉ์ง */
|
| 593 |
-
margin-bottom: 0 !important; /* ์๋์ชฝ ๊ฐ๊ฒฉ ์์ ์ ๊ฑฐ */
|
| 594 |
-
padding-bottom: 0 !important;
|
| 595 |
-
height: auto !important; /* ๊ป๋ฐ๊ธฐ ๋์ด ๊ณ ์ ํด์ */
|
| 596 |
-
min-height: unset !important;
|
| 597 |
-
}
|
| 598 |
-
.placeholder .prose {
|
| 599 |
-
background: #f8fafc !important; /* ์ฟจํค ํ์ */
|
| 600 |
-
border: 1px solid #e2e8f0 !important;
|
| 601 |
-
border-bottom: none !important; /* ์๋์ชฝ ๋ฅ๊ทผ ์ ์ ๊ฑฐ๋ก ๊ฒฐํฉ ์ค๋น */
|
| 602 |
-
border-radius: 12px 12px 0 0 !important; /* ์๋ ๋ชจ์๋ฆฌ ์ง๊ฐ */
|
| 603 |
-
padding: 24px 24px 10px 24px !important;
|
| 604 |
-
max-width: 800px !important;
|
| 605 |
-
width: 100% !important;
|
| 606 |
-
margin: 0 auto !important;
|
| 607 |
-
position: relative !important;
|
| 608 |
-
z-index: 2 !important;
|
| 609 |
-
display: block !important; /* ์์ ๋ง๋ฒ ๋ฐฉ์ด */
|
| 610 |
-
visibility: visible !important;
|
| 611 |
-
color: #334155 !important;
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
/* 2x2 grid layout for chatbot example buttons (Stitch Action Grid style) */
|
| 615 |
-
[class*="examples"], .gr-samples-wrapper, .examples-container {
|
| 616 |
-
display: grid !important;
|
| 617 |
-
grid-template-columns: repeat(2, 1fr) !important;
|
| 618 |
-
gap: 10px !important;
|
| 619 |
-
|
| 620 |
-
/* ๐ ํต์ฌ: ์ฐ์ปด ๋ณด๋์ ํ ๋ชธ์ด ๋๊ธฐ ์ํ ์๋ฒฝ ๋ฐ์ฐฉ ๋ฐ ๊ป๋ฐ๊ธฐ ๐ */
|
| 621 |
-
margin: -1px auto 40px auto !important; /* ์ฐ์ปด ๋ณด๋ ๋ฐ๋ก ๋ฐ์ ์ฐฐ์น ๋ถ์ */
|
| 622 |
-
background: #f8fafc !important; /* ์ฐ์ปด ๋ณด๋์ ๋์ผํ ํ์ ๋ฐฐ๊ฒฝ */
|
| 623 |
-
border: 1px solid #e2e8f0 !important;
|
| 624 |
-
border-top: none !important; /* ์์ ์ ๊ฑฐ๋ก ํ ๋ชธ ๊ฒฐํฉ */
|
| 625 |
-
border-radius: 0 0 12px 12px !important; /* ์ ๋ชจ์๋ฆฌ ์ง๊ฐ */
|
| 626 |
-
padding: 5px 24px 24px 24px !important;
|
| 627 |
-
max-width: 800px !important; /* ์ฐ์ปด ๋ณด๋ ๋๋น ์ผ์น */
|
| 628 |
-
width: 100% !important;
|
| 629 |
-
position: relative !important;
|
| 630 |
-
z-index: 1 !important; /* ๊ฒน์น ๋ ํฐ ์ ์๋ณด์ด๊ฒ ๋ฐ์ผ๋ก */
|
| 631 |
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
|
| 632 |
-
}
|
| 633 |
-
[class*="examples"] button {
|
| 634 |
-
text-align: center !important; /* ์ฌ์ฉ์ ์์ฒญ: ๊ฐ์ด๋ฐ ์ ๋ ฌ */
|
| 635 |
-
padding: 14px 18px !important;
|
| 636 |
-
background: #e0f2fe !important; /* ์ฌ์ฉ์ ์์ฒญ: ์ฐํ ํ๋์(ํ๋) ๋ฐฐ๊ฒฝ */
|
| 637 |
-
border: 1px solid #bae6fd !important;
|
| 638 |
-
border-radius: 10px !important;
|
| 639 |
-
font-size: 13px !important;
|
| 640 |
-
font-weight: 600 !important;
|
| 641 |
-
color: #000000 !important; /* ์ฌ์ฉ์ ์์ฒญ: ๊ฒ์์ ํ
์คํธ */
|
| 642 |
-
line-height: 1.4 !important;
|
| 643 |
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02) !important;
|
| 644 |
-
transition: all 0.2s ease-in-out !important;
|
| 645 |
-
white-space: normal !important;
|
| 646 |
-
height: auto !important;
|
| 647 |
-
min-height: 54px !important;
|
| 648 |
-
cursor: pointer !important;
|
| 649 |
-
display: flex !important;
|
| 650 |
-
align-items: center !important;
|
| 651 |
-
justify-content: center !important;
|
| 652 |
-
}
|
| 653 |
-
.dark [class*="examples"] button {
|
| 654 |
-
background: rgba(30, 41, 59, 0.5) !important;
|
| 655 |
-
border-color: rgba(148, 163, 184, 0.2) !important;
|
| 656 |
-
color: #e2e8f0 !important;
|
| 657 |
-
}
|
| 658 |
-
[class*="examples"] button:hover {
|
| 659 |
-
transform: translateY(-1px) !important;
|
| 660 |
-
background: #bae6fd !important; /* ์ฝ๊ฐ ์ง์ ํ๋์ ํธ๋ฒ */
|
| 661 |
-
border-color: #7dd3fc !important;
|
| 662 |
-
color: #1e3a8a !important; /* ์ฌ์ฉ์ ์์ฒญ: ์งํ ํ๋์ ํ
์คํธ */
|
| 663 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06) !important;
|
| 664 |
-
}
|
| 665 |
-
.dark [class*="examples"] button:hover {
|
| 666 |
-
background: rgba(30, 41, 59, 0.85) !important;
|
| 667 |
-
border-color: rgba(148, 163, 184, 0.4) !important;
|
| 668 |
-
color: #ffffff !important;
|
| 669 |
-
}
|
| 670 |
-
|
| 671 |
-
/* ์ฑ๋ด ์ ์ก ๋ฒํผ ํ๋ฆฌ๋ฏธ์ ๋คํฌ ์ฌ๋ ์ดํธ ์คํ์ผ (์ด์ํ ๊ทธ๋ผ๋ฐ์ด์
์ ๊ฑฐ, ๋จ์ ํ๊ณ ์ผ๊ด์ฑ ์๋ ์๊ฐ) */
|
| 672 |
-
button.primary,
|
| 673 |
-
.primary-btn,
|
| 674 |
-
button.lg.primary,
|
| 675 |
-
button.sm.primary,
|
| 676 |
-
button.variant-primary,
|
| 677 |
-
button[class*="submit-btn"],
|
| 678 |
-
[data-testid="submit-button"] {
|
| 679 |
-
background: #1e293b !important; /* ํ๋ฆฌ๋ฏธ์ ๋คํฌ ์ฌ๋ ์ดํธ ์ฐจ์ฝ */
|
| 680 |
-
color: white !important;
|
| 681 |
-
font-weight: 700 !important;
|
| 682 |
-
border: 1px solid #0f172a !important;
|
| 683 |
-
border-radius: 8px !important;
|
| 684 |
-
box-shadow: 0 2px 4px rgba(15, 23, 42, 0.08) !important;
|
| 685 |
-
transition: all 0.2s ease-in-out !important;
|
| 686 |
-
cursor: pointer !important;
|
| 687 |
-
}
|
| 688 |
-
button.primary:hover,
|
| 689 |
-
.primary-btn:hover,
|
| 690 |
-
button.variant-primary:hover,
|
| 691 |
-
button[class*="submit-btn"]:hover,
|
| 692 |
-
[data-testid="submit-button"]:hover {
|
| 693 |
-
background: #0f172a !important; /* ํธ๋ฒ ์ ๋ฅ ๋ธ๋ */
|
| 694 |
-
box-shadow: 0 4px 8px rgba(15, 23, 42, 0.15) !important;
|
| 695 |
-
transform: translateY(-1px) !important;
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
/* secondary ๋ฐ ๊ธฐํ ์ ํธ๋ฆฌํฐ ๋ฒํผ ์คํ์ผ */
|
| 699 |
-
/* secondary ๋ฐ ๊ธฐํ ์ ํธ๋ฆฌํฐ ๋ฒํผ ์คํ์ผ (๋ณด๋ผ์ ์ ๊ฑฐ) */
|
| 700 |
-
button.secondary,
|
| 701 |
-
button.lg.secondary,
|
| 702 |
-
button.sm.secondary,
|
| 703 |
-
button.wrap,
|
| 704 |
-
button.variant-secondary,
|
| 705 |
-
.secondary-btn {
|
| 706 |
-
background-color: rgba(255, 255, 255, 0.6) !important;
|
| 707 |
-
color: #0f172a !important; /* ๊ธฐ๋ณธ ๊ฒ์ ์ */
|
| 708 |
-
border: 1px solid #cbd5e1 !important;
|
| 709 |
-
font-weight: 700 !important;
|
| 710 |
-
transition: all 0.2s ease-in-out !important;
|
| 711 |
-
backdrop-filter: blur(8px);
|
| 712 |
-
}
|
| 713 |
-
.dark button.secondary,
|
| 714 |
-
.dark button.variant-secondary,
|
| 715 |
-
.dark .secondary-btn {
|
| 716 |
-
background-color: rgba(30, 41, 59, 0.6) !important;
|
| 717 |
-
color: #f1f5f9 !important;
|
| 718 |
-
border-color: rgba(14, 165, 233, 0.2) !important;
|
| 719 |
-
}
|
| 720 |
-
button.secondary:hover,
|
| 721 |
-
button.variant-secondary:hover,
|
| 722 |
-
.secondary-btn:hover {
|
| 723 |
-
background-color: rgba(255, 255, 255, 0.95) !important;
|
| 724 |
-
color: #0f172a !important;
|
| 725 |
-
border-color: #94a3b8 !important;
|
| 726 |
-
}
|
| 727 |
-
.dark button.secondary:hover,
|
| 728 |
-
.dark button.variant-secondary:hover {
|
| 729 |
-
background-color: rgba(30, 41, 59, 0.95) !important;
|
| 730 |
-
color: white !important;
|
| 731 |
-
border-color: #38bdf8 !important;
|
| 732 |
-
}
|
| 733 |
-
|
| 734 |
-
/* ์ฑ๋ด ๋ณด๋ผ์ ๋ฐฐ๊ฒฝ ์์ ์ ๊ฑฐ ๋ฐ ๊ณ ๋๋น ์ฌ๋ ์ดํธ/ํ์ดํธ ๋ฒ๋ธ ๊ตฌํ */
|
| 735 |
-
.bubble, .message {
|
| 736 |
-
border-radius: 12px !important;
|
| 737 |
-
}
|
| 738 |
-
|
| 739 |
-
/* ์ฌ์ฉ์ ๋ฒ๋ธ ๊ฐ๋
์ฑ ์์ ๊ฐ์ (๊ธ์จ์์ ๊ฐ์ ๋ก ๊นจ๋ํ ํฐ์์ผ๋ก ๊ณ ์ ํ์ฌ 500% ์ ๋ช
ํ๊ฒ ํ์) */
|
| 740 |
-
.message.user {
|
| 741 |
-
background-color: #334155 !important; /* ์ฐจ๋ถํ๊ณ ๊ณ ๊ธ์ค๋ฌ์ด ๋คํฌ ์ฌ๋ ์ดํธ */
|
| 742 |
-
border: 1px solid rgba(51, 65, 85, 0.2) !important;
|
| 743 |
-
}
|
| 744 |
-
.message.user p, .message.user span, .message.user li, .message.user div {
|
| 745 |
-
color: #ffffff !important; /* ์์ ํฐ์ ๊ธ์จ */
|
| 746 |
-
font-weight: 600 !important;
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
/* ๋ด ๋ฒ๋ธ ๊ฐ๋
์ฑ ์์ ๊ฐ์ (๊ธ์จ์ ํ์คํ ๊ฒ์ ์, ๋ณด๋ผ์ ํ
๋๋ฆฌ ์ ๊ฑฐ) */
|
| 750 |
-
.message.bot {
|
| 751 |
-
background-color: rgba(255, 255, 255, 0.95) !important; /* ๋ฐํฌ๋ช
๊นจ๋ํ ํ์ดํธ ๊ธ๋์ค */
|
| 752 |
-
border: 1px solid #cbd5e1 !important;
|
| 753 |
-
}
|
| 754 |
-
.message.bot p, .message.bot span, .message.bot li, .message.bot div {
|
| 755 |
-
color: #0f172a !important; /* ํ์คํ ๊ณ ๋๋น ๊ฒ์ ์ ๊ธ์จ */
|
| 756 |
-
}
|
| 757 |
-
.dark .message.user {
|
| 758 |
-
background-color: #475569 !important;
|
| 759 |
-
}
|
| 760 |
-
.dark .message.bot {
|
| 761 |
-
background-color: rgba(30, 41, 59, 0.85) !important;
|
| 762 |
-
border-color: rgba(14, 165, 233, 0.2) !important;
|
| 763 |
-
}
|
| 764 |
-
.dark .message.bot p, .dark .message.bot span, .dark .message.bot li {
|
| 765 |
-
color: #f1f5f9 !important;
|
| 766 |
-
}
|
| 767 |
-
|
| 768 |
-
/* Chatbot ๋ผ๋ฒจ/ํญ ์์ ์จ๊น (๋ถํ์ํ ๋ณด๋ผ์/ํ๋์ ์์ ๋ฐ ํ
๋๋ฆฌ ์์ฒ ์ฐจ๋จ) */
|
| 769 |
-
/* ์ฃผ์: .chatbot > div:first-child ๋ ์ฐ์ปด์นด๋๋ฅผ ์ง์๋ฒ๋ฆด ์ ์์ผ๋ฏ๋ก ์ญ์ ! */
|
| 770 |
-
.chatbot-label,
|
| 771 |
-
div[class*="chatbot"] .label,
|
| 772 |
-
[data-testid="chatbot"] .label,
|
| 773 |
-
.chatbot-header,
|
| 774 |
-
.gr-panel-title,
|
| 775 |
-
.gr-chatbot-label,
|
| 776 |
-
.label-wrap {
|
| 777 |
-
display: none !important;
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
/* ์ฑ๋ด ๋ฉ์ธ ์ปจํ
์ด๋ ํฌ๋ช
ํ ๋ฐ ํ
๋๋ฆฌ ๊น๋ํ */
|
| 781 |
-
.chatbot, [class*="chatbot"] {
|
| 782 |
-
background: rgba(255, 255, 255, 0.3) !important;
|
| 783 |
-
border: 1px solid #cbd5e1 !important;
|
| 784 |
-
border-radius: 12px !important;
|
| 785 |
-
}
|
| 786 |
-
.dark .chatbot {
|
| 787 |
-
background: rgba(15, 23, 42, 0.3) !important;
|
| 788 |
-
border-color: rgba(14, 165, 233, 0.15) !important;
|
| 789 |
-
}
|
| 790 |
-
|
| 791 |
-
/* ์
๋ ฅ์ฐฝ(ํ
์คํธ์์ด๋ฆฌ์ด) ์ธ๋ก ๋์ด ๋ฐ ๋จ์ผ ํ ์์ง ์ค์ ์ ๋ ฌ ์ต์ ํ */
|
| 792 |
-
textarea,
|
| 793 |
-
[class*="input-container"] textarea,
|
| 794 |
-
[data-testid="textbox"] textarea {
|
| 795 |
-
height: 48px !important;
|
| 796 |
-
min-height: 48px !important;
|
| 797 |
-
max-height: 48px !important;
|
| 798 |
-
font-size: 13px !important;
|
| 799 |
-
padding: 13px 16px !important; /* ์์๋ ํจ๋ฉ์ ์ค์ฌ์ ๋์ด์ ๋ง๊ฒ ์กฐ์ */
|
| 800 |
-
line-height: 1.5 !important;
|
| 801 |
-
border-radius: 8px !important;
|
| 802 |
-
border: 1px solid #cbd5e1 !important;
|
| 803 |
-
background: rgba(255, 255, 255, 0.8) !important;
|
| 804 |
-
color: #0f172a !important; /* ์
๋ ฅ ํ
์คํธ ๊ฒ์ ์ */
|
| 805 |
-
resize: none !important; /* ์ธ๋ก ํฌ๊ธฐ ์กฐ์ ๋ฐฉ์ง */
|
| 806 |
-
overflow-y: hidden !important; /* ์คํฌ๋กค๋ฐ ๊ฐ์ถค */
|
| 807 |
-
box-sizing: border-box !important;
|
| 808 |
-
}
|
| 809 |
-
textarea:focus {
|
| 810 |
-
border-color: #0ea5e9 !important; /* ํฌ์ปค์ค ์ ์ค์นด์ด ๋ธ๋ฃจ */
|
| 811 |
-
background: #ffffff !important;
|
| 812 |
-
}
|
| 813 |
-
.dark textarea {
|
| 814 |
-
background: rgba(30, 41, 59, 0.8) !important;
|
| 815 |
-
border-color: rgba(14, 165, 233, 0.25) !important;
|
| 816 |
-
color: white !important;
|
| 817 |
-
}
|
| 818 |
-
|
| 819 |
-
/* ์ฑ๋ด ์
๋ ฅ์ฐฝ๊ณผ ์ ์ก ๋ฒํผ ์ธ๋ก ๋์ด ์๋ฒฝ ๋๊ธฐํ ๋ฐ ์ฌ๋ฐฑ ๋ถ๋ฆฌ */
|
| 820 |
-
button[class*="submit-btn"],
|
| 821 |
-
[data-testid="submit-button"],
|
| 822 |
-
#submit-btn {
|
| 823 |
-
margin-left: 12px !important;
|
| 824 |
-
border-radius: 8px !important;
|
| 825 |
-
min-width: 95px !important;
|
| 826 |
-
height: 48px !important; /* ์
๋ ฅ์ฐฝ์ height(48px)์ 100% ๋์ผํ๊ฒ ์ผ์น */
|
| 827 |
-
padding: 0 16px !important;
|
| 828 |
-
display: flex !important;
|
| 829 |
-
align-items: center !important;
|
| 830 |
-
justify-content: center !important;
|
| 831 |
-
box-sizing: border-box !important;
|
| 832 |
-
}
|
| 833 |
-
div:has(> button[class*="submit-btn"]),
|
| 834 |
-
div:has(> [data-testid="submit-button"]),
|
| 835 |
-
.input-container,
|
| 836 |
-
[class*="input-container"] {
|
| 837 |
-
gap: 12px !important;
|
| 838 |
-
align-items: center !important; /* ์์ง์ถ ๊ธฐ์ค์ผ๋ก ์ค์ ์ ๋ ฌ */
|
| 839 |
-
}
|
| 840 |
-
|
| 841 |
-
/* ์ฑ๋ด ๋ต๋ณ ๋งํฌ๋ค์ด ๊ฐ๋
์ฑ ๋ฐ ์๊ฐ/์ค๊ฐ๊ฒฉ ์ต์ ํ (์ธ๋ผ์ธ ์์์ ์์ง ์ /๋ณด๋ ์์ฒ ์ฐจ๋จ) */
|
| 842 |
-
.message p, .message li, [class*="message"] p, [class*="message"] li {
|
| 843 |
-
line-height: 1.68 !important;
|
| 844 |
-
margin-bottom: 14px !important;
|
| 845 |
-
letter-spacing: -0.01em !important;
|
| 846 |
-
border: none !important;
|
| 847 |
-
border-left: none !important;
|
| 848 |
-
border-right: none !important;
|
| 849 |
-
box-shadow: none !important;
|
| 850 |
-
}
|
| 851 |
-
.message blockquote, [class*="message"] blockquote {
|
| 852 |
-
border: none !important;
|
| 853 |
-
border-left: none !important;
|
| 854 |
-
border-right: none !important;
|
| 855 |
-
padding: 0 !important;
|
| 856 |
-
margin: 0 !important;
|
| 857 |
-
box-shadow: none !important;
|
| 858 |
-
}
|
| 859 |
-
.message h3, [class*="message"] h3 {
|
| 860 |
-
margin-top: 24px !important;
|
| 861 |
-
margin-bottom: 12px !important;
|
| 862 |
-
font-weight: 800 !important;
|
| 863 |
-
}
|
| 864 |
-
"""
|
| 865 |
|
| 866 |
CHATBOT_DESCRIPTION = """
|
| 867 |
<div class="prose">
|
|
@@ -904,14 +353,14 @@ blocks_kwargs = {}
|
|
| 904 |
if gradio_major < 5:
|
| 905 |
interface_kwargs["theme"] = theme_obj
|
| 906 |
blocks_kwargs["theme"] = theme_obj
|
| 907 |
-
blocks_kwargs["css"] =
|
| 908 |
elif gradio_major < 6:
|
| 909 |
launch_kwargs["theme"] = theme_obj
|
| 910 |
blocks_kwargs["theme"] = theme_obj
|
| 911 |
-
blocks_kwargs["css"] =
|
| 912 |
else:
|
| 913 |
launch_kwargs["theme"] = theme_obj
|
| 914 |
-
launch_kwargs["css"] =
|
| 915 |
|
| 916 |
# Blocks๋ฅผ ํ์ฉํ 2์ปฌ๋ผ ๋ ์ด์์ ๋์๋ณด๋ ๊ฐํธ
|
| 917 |
with gr.Blocks(**blocks_kwargs) as demo:
|
|
|
|
| 14 |
import gradio as gr
|
| 15 |
from langgraph.graph import END, StateGraph
|
| 16 |
|
| 17 |
+
from src.retrieval.finRetrieval import HybridResult, graphrag
|
| 18 |
+
from src.utils.ui_templates import CUSTOM_CSS, build_stats_html
|
| 19 |
|
| 20 |
dotenv.load_dotenv()
|
| 21 |
|
|
|
|
| 45 |
class ChatState(TypedDict):
|
| 46 |
question: str # ์ฌ์ฉ์ ์ง๋ฌธ
|
| 47 |
history: List[dict] # ๋ํ ํ์คํ ๋ฆฌ [{"role": "user"/"assistant", "content": "..."}]
|
| 48 |
+
context: str # GraphRAG ๊ฒ์ ๊ฒฐ๊ณผ ๋๋ ์ผ๋ฐ ์ง์ ๋ต๋ณ
|
| 49 |
+
answer: str # ์ต์ข
๋ต๋ณ
|
| 50 |
+
mode: str # "graph": ๊ทธ๋ํ ๊ธฐ๋ฐ | "general": ์ผ๋ฐ ์ง์ ๊ธฐ๋ฐ
|
| 51 |
|
| 52 |
|
| 53 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def retrieve_node(state: ChatState) -> ChatState:
|
| 59 |
+
"""Node 1: search_with_fallback์ผ๋ก ๊ทธ๋ํ ๊ฒ์ ๋๋ ์ผ๋ฐ ์ง์ ์๋ต ๋ผ์ฐํ
"""
|
| 60 |
try:
|
| 61 |
+
hybrid: HybridResult = graphrag.search_with_fallback(
|
| 62 |
+
query_text=state["question"],
|
| 63 |
+
history=state["history"],
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if hybrid.mode == "general":
|
| 67 |
+
# ์ผ๋ฐ ์ง์ ๋ชจ๋: ๋ฐฐ๋ + GPT-4o ๋ต๋ณ ๋ฐํ
|
| 68 |
+
disclaimer = (
|
| 69 |
+
"> โ ๏ธ **์ง์ ๊ทธ๋ํ์์ ๊ด๋ จ ๋ด์ค๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.**\n"
|
| 70 |
+
"> GPT-4o์ ์ผ๋ฐ ํ์ต ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ต๋ณํฉ๋๋ค.\n"
|
| 71 |
+
"> ์ต์ ๊ตญ๋ด ๋ด์ค ๊ธฐ๋ฐ ์ ๋ณด๊ฐ ํ์ํ๋ค๋ฉด ์ง๋ฌธ์ ๋ ๊ตฌ์ฒด์ ์ผ๋ก ์
๋ ฅํด ๋ณด์ธ์.\n\n"
|
| 72 |
+
"---\n\n"
|
| 73 |
+
)
|
| 74 |
+
context = disclaimer + hybrid.answer
|
| 75 |
+
return {**state, "context": context, "mode": "general"}
|
| 76 |
+
|
| 77 |
+
# ๊ทธ๋ํ ๊ธฐ๋ฐ ๋ชจ๋: ๊ธฐ์กด ์ถ์ฒ ์ถ์ถ + ๋ด์ค ํผ๋ ๋ก์ง
|
| 78 |
+
context = hybrid.answer
|
| 79 |
sources = []
|
| 80 |
+
seen_urls: set = set()
|
| 81 |
+
|
| 82 |
+
# retriever_result์์ ์์ 3๊ฐ ๋ด์ค ์ถ์ฒ ์ถ์ถ
|
| 83 |
+
retriever_result = hybrid.retriever_result
|
| 84 |
+
if retriever_result and hasattr(retriever_result, "items"):
|
| 85 |
+
for item in retriever_result.items:
|
| 86 |
meta = getattr(item, "metadata", {})
|
| 87 |
title = meta.get("article_title")
|
| 88 |
url = meta.get("article_url")
|
|
|
|
| 161 |
|
| 162 |
except Exception as e:
|
| 163 |
context = f"[๊ฒ์ ์ค๋ฅ: {e}]"
|
| 164 |
+
return {**state, "context": context, "mode": state.get("mode", "graph")}
|
| 165 |
|
| 166 |
|
| 167 |
def generate_node(state: ChatState) -> ChatState:
|
| 168 |
"""Node 2: ๋ํ ํ์คํ ๋ฆฌ๋ฅผ ๊ณ ๋ คํ์ฌ ์ต์ข
๋ต๋ณ ์์ฑ
|
| 169 |
|
| 170 |
+
GraphRAG(graph ๋ชจ๋) ๋๋ ์ผ๋ฐ ์ง์(general ๋ชจ๋) ์๋ต ๋ชจ๋
|
| 171 |
+
retrieve_node์์ context์ ์ต์ข
ํ
์คํธ๋ฅผ ๋ด์์ฃผ๋ฏ๋ก ๊ทธ๋๋ก ์ฌ์ฉํฉ๋๋ค.
|
| 172 |
"""
|
|
|
|
|
|
|
| 173 |
answer = state["context"] if state["context"] else "๊ด๋ จ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."
|
| 174 |
return {**state, "answer": answer}
|
| 175 |
|
|
|
|
| 214 |
"history": history,
|
| 215 |
"context": "",
|
| 216 |
"answer": "",
|
| 217 |
+
"mode": "",
|
| 218 |
}
|
| 219 |
|
| 220 |
yield "๐ ์ค์๊ฐ ์ง์ ๊ทธ๋ํ์์ ๊ด๋ จ ๋ด์ค๋ฅผ ๊ฒ์ํ๋ ์ค์
๋๋ค..."
|
|
|
|
| 223 |
# LangGraph์ stream์ ์ฌ์ฉํ์ฌ ๊ฐ ๋
ธ๋ ์คํ ์์ ๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์
|
| 224 |
for event in chat_graph.stream(state):
|
| 225 |
if "retrieve" in event:
|
| 226 |
+
retrieved_mode = event["retrieve"].get("mode", "graph")
|
| 227 |
+
if retrieved_mode == "general":
|
| 228 |
+
yield "๐ ๊ด๋ จ ๋ด์ค ์์ โ GPT-4o ์ผ๋ฐ ์ง์์ผ๋ก ๋ต๋ณ์ ์์ฑํ๋ ์ค์
๋๋ค..."
|
| 229 |
+
else:
|
| 230 |
+
yield "๐ก ๊ฒ์ ์๋ฃ! ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ต์ข
๋ต๋ณ์ ์์ฑํ๋ ์ค์
๋๋ค..."
|
| 231 |
elif "generate" in event:
|
| 232 |
yield event["generate"]["answer"]
|
| 233 |
except Exception as e:
|
|
|
|
| 295 |
return stats
|
| 296 |
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 299 |
# 5. Gradio UI ๊ตฌ์ฑ
|
| 300 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 311 |
secondary_hue="slate",
|
| 312 |
)
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
CHATBOT_DESCRIPTION = """
|
| 316 |
<div class="prose">
|
|
|
|
| 353 |
if gradio_major < 5:
|
| 354 |
interface_kwargs["theme"] = theme_obj
|
| 355 |
blocks_kwargs["theme"] = theme_obj
|
| 356 |
+
blocks_kwargs["css"] = CUSTOM_CSS
|
| 357 |
elif gradio_major < 6:
|
| 358 |
launch_kwargs["theme"] = theme_obj
|
| 359 |
blocks_kwargs["theme"] = theme_obj
|
| 360 |
+
blocks_kwargs["css"] = CUSTOM_CSS
|
| 361 |
else:
|
| 362 |
launch_kwargs["theme"] = theme_obj
|
| 363 |
+
launch_kwargs["css"] = CUSTOM_CSS
|
| 364 |
|
| 365 |
# Blocks๋ฅผ ํ์ฉํ 2์ปฌ๋ผ ๋ ์ด์์ ๋์๋ณด๋ ๊ฐํธ
|
| 366 |
with gr.Blocks(**blocks_kwargs) as demo:
|
src/retrieval/finRetrieval.py
CHANGED
|
@@ -12,6 +12,8 @@ app.py์์ importํ์ฌ Gradio ์ฑ๋ด๊ณผ ์ฐ๋ํฉ๋๋ค.
|
|
| 12 |
|
| 13 |
import logging
|
| 14 |
import os
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Neo4j DBMS server warning (Deprecated vector queryNodes ๋ฑ) ๋ก๊น
์ฐจ๋จ
|
| 17 |
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
@@ -31,6 +33,15 @@ from neo4j_graphrag.retrievers import (
|
|
| 31 |
dotenv.load_dotenv()
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def get_neo4j_driver() -> neo4j.Driver:
|
| 35 |
uri = os.getenv("NEO4J_URI", "neo4j://localhost:7687")
|
| 36 |
client_id = os.getenv("NEO4J_CLIENT_ID")
|
|
@@ -136,7 +147,6 @@ CYPHER QUERY:
|
|
| 136 |
# 3. ToolsRetriever + GraphRAG ์กฐ๋ฆฝ
|
| 137 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 138 |
|
| 139 |
-
from typing import Any
|
| 140 |
|
| 141 |
from neo4j_graphrag.retrievers.base import Retriever
|
| 142 |
from neo4j_graphrag.types import RawSearchResult, RetrieverResult
|
|
@@ -230,17 +240,20 @@ _prompt_template = CustomRagTemplate(
|
|
| 230 |
|
| 231 |
class LazyGraphRAG:
|
| 232 |
"""์ํฌํธ ์์ ์ DB ์ฐ๊ฒฐ์ ๋ฐฉ์งํ๊ณ ์ค์ ํธ์ถ๋ ๋ GraphRAG ์ธ์คํด์ค๋ฅผ ์ด๊ธฐํํ๋ ์ง์ฐ ํ๊ฐ ํ๋ก์"""
|
|
|
|
| 233 |
def __init__(self) -> None:
|
| 234 |
self._graphrag: Any = None
|
|
|
|
|
|
|
| 235 |
|
| 236 |
def _init_once(self) -> None:
|
| 237 |
if self._graphrag is not None:
|
| 238 |
return
|
| 239 |
|
| 240 |
# OpenAI ํด๋ผ์ด์ธํธ ๋ฐ ์๋ฒ ๋ ์ง์ฐ ์ด๊ธฐํ (CI ํฌ๋์ ๋ฐฉ์ง)
|
| 241 |
-
|
| 242 |
embedder = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 243 |
-
|
| 244 |
driver = get_neo4j_driver()
|
| 245 |
|
| 246 |
vector_cypher_retriever = VectorCypherRetriever(
|
|
@@ -252,14 +265,14 @@ class LazyGraphRAG:
|
|
| 252 |
|
| 253 |
text2cypher_retriever = Text2CypherRetriever(
|
| 254 |
driver=driver,
|
| 255 |
-
llm=
|
| 256 |
neo4j_schema=_get_schema(driver),
|
| 257 |
examples=_examples,
|
| 258 |
)
|
| 259 |
|
| 260 |
tools_retriever = ToolsRetriever(
|
| 261 |
driver=driver,
|
| 262 |
-
llm=
|
| 263 |
tools=[
|
| 264 |
vector_cypher_retriever.convert_to_tool(
|
| 265 |
name="vector_retriever",
|
|
@@ -271,18 +284,134 @@ class LazyGraphRAG:
|
|
| 271 |
),
|
| 272 |
],
|
| 273 |
)
|
| 274 |
-
|
| 275 |
-
|
| 276 |
tools_retriever=tools_retriever,
|
| 277 |
fallback_retriever=vector_cypher_retriever,
|
| 278 |
)
|
| 279 |
-
|
| 280 |
self._graphrag = GraphRAG(
|
| 281 |
-
llm=
|
| 282 |
-
retriever=
|
| 283 |
prompt_template=_prompt_template,
|
| 284 |
)
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
def search(self, *args: Any, **kwargs: Any) -> Any:
|
| 287 |
self._init_once()
|
| 288 |
assert self._graphrag is not None
|
|
|
|
| 12 |
|
| 13 |
import logging
|
| 14 |
import os
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Any
|
| 17 |
|
| 18 |
# Neo4j DBMS server warning (Deprecated vector queryNodes ๋ฑ) ๋ก๊น
์ฐจ๋จ
|
| 19 |
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
|
|
| 33 |
dotenv.load_dotenv()
|
| 34 |
|
| 35 |
|
| 36 |
+
@dataclass
|
| 37 |
+
class HybridResult:
|
| 38 |
+
"""GraphRAG ๋๋ ์ผ๋ฐ ์ง์ ๊ธฐ๋ฐ ํตํฉ ์๋ต ๊ฒฐ๊ณผ"""
|
| 39 |
+
|
| 40 |
+
answer: str # ์ต์ข
๋ต๋ณ ๋ฌธ์์ด
|
| 41 |
+
mode: str # "graph": ๊ทธ๋ํ ๊ฒ์ ๊ธฐ๋ฐ | "general": GPT-4o ์ผ๋ฐ ์ง์ ๊ธฐ๋ฐ
|
| 42 |
+
retriever_result: Any = None # RetrieverResult (mode="graph"์ผ ๋๋ง ์ ํจ)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
def get_neo4j_driver() -> neo4j.Driver:
|
| 46 |
uri = os.getenv("NEO4J_URI", "neo4j://localhost:7687")
|
| 47 |
client_id = os.getenv("NEO4J_CLIENT_ID")
|
|
|
|
| 147 |
# 3. ToolsRetriever + GraphRAG ์กฐ๋ฆฝ
|
| 148 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 149 |
|
|
|
|
| 150 |
|
| 151 |
from neo4j_graphrag.retrievers.base import Retriever
|
| 152 |
from neo4j_graphrag.types import RawSearchResult, RetrieverResult
|
|
|
|
| 240 |
|
| 241 |
class LazyGraphRAG:
|
| 242 |
"""์ํฌํธ ์์ ์ DB ์ฐ๊ฒฐ์ ๋ฐฉ์งํ๊ณ ์ค์ ํธ์ถ๋ ๋ GraphRAG ์ธ์คํด์ค๋ฅผ ์ด๊ธฐํํ๋ ์ง์ฐ ํ๊ฐ ํ๋ก์"""
|
| 243 |
+
|
| 244 |
def __init__(self) -> None:
|
| 245 |
self._graphrag: Any = None
|
| 246 |
+
self._hybrid_retriever: Any = None # ํ์ง ํ๊ฐ์ฉ ์ง์ ์ ๊ทผ ๊ฐ๋ฅํ ๋ฆฌํธ๋ฆฌ๋ฒ
|
| 247 |
+
self._rag_llm: Any = None # ์ผ๋ฐ ์ง์ ๋ต๋ณ ์์ฑ์ฉ LLM
|
| 248 |
|
| 249 |
def _init_once(self) -> None:
|
| 250 |
if self._graphrag is not None:
|
| 251 |
return
|
| 252 |
|
| 253 |
# OpenAI ํด๋ผ์ด์ธํธ ๋ฐ ์๋ฒ ๋ ์ง์ฐ ์ด๊ธฐํ (CI ํฌ๋์ ๋ฐฉ์ง)
|
| 254 |
+
self._rag_llm = OpenAILLM(model_name="gpt-4o", model_params={"temperature": 0})
|
| 255 |
embedder = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 256 |
+
|
| 257 |
driver = get_neo4j_driver()
|
| 258 |
|
| 259 |
vector_cypher_retriever = VectorCypherRetriever(
|
|
|
|
| 265 |
|
| 266 |
text2cypher_retriever = Text2CypherRetriever(
|
| 267 |
driver=driver,
|
| 268 |
+
llm=self._rag_llm,
|
| 269 |
neo4j_schema=_get_schema(driver),
|
| 270 |
examples=_examples,
|
| 271 |
)
|
| 272 |
|
| 273 |
tools_retriever = ToolsRetriever(
|
| 274 |
driver=driver,
|
| 275 |
+
llm=self._rag_llm,
|
| 276 |
tools=[
|
| 277 |
vector_cypher_retriever.convert_to_tool(
|
| 278 |
name="vector_retriever",
|
|
|
|
| 284 |
),
|
| 285 |
],
|
| 286 |
)
|
| 287 |
+
|
| 288 |
+
self._hybrid_retriever = HybridFallbackRetriever(
|
| 289 |
tools_retriever=tools_retriever,
|
| 290 |
fallback_retriever=vector_cypher_retriever,
|
| 291 |
)
|
| 292 |
+
|
| 293 |
self._graphrag = GraphRAG(
|
| 294 |
+
llm=self._rag_llm,
|
| 295 |
+
retriever=self._hybrid_retriever,
|
| 296 |
prompt_template=_prompt_template,
|
| 297 |
)
|
| 298 |
|
| 299 |
+
def _is_context_sufficient(self, query_text: str, history: list, retriever_result: Any) -> bool:
|
| 300 |
+
"""๊ฒ์๋ ์ปจํ
์คํธ๊ฐ ์ง๋ฌธ ๋ฐ ์ด์ ๋ํ ํ๋ฆ์ ์ค์ง์ ์ผ๋ก ๋์์ด ๋๋ ๊ธ์ต/๊ธฐ์ ๋ด์ค ๋ฐ์ดํฐ์ธ์ง GPT-4o๋ก ํ๋จ"""
|
| 301 |
+
if retriever_result is None:
|
| 302 |
+
return False
|
| 303 |
+
if not hasattr(retriever_result, "items") or not retriever_result.items:
|
| 304 |
+
return False
|
| 305 |
+
total_content = " ".join(
|
| 306 |
+
getattr(item, "content", "") for item in retriever_result.items
|
| 307 |
+
).strip()
|
| 308 |
+
if len(total_content) < 100:
|
| 309 |
+
return False
|
| 310 |
+
|
| 311 |
+
# GPT-4o ๊ธฐ๋ฐ ์ง๋ฅ์ ์๊ฐ ์ง๋จ (์ด์ ๋ํ ํ์คํ ๋ฆฌ ๋ฐ ์ง๋ฌธ์ ๋งฅ๋ฝ ๊ฒฐํฉ ํ์ )
|
| 312 |
+
try:
|
| 313 |
+
assert self._rag_llm is not None
|
| 314 |
+
context_snippet = total_content[:800]
|
| 315 |
+
|
| 316 |
+
# ์ด์ ๋ํ ํ์คํ ๋ฆฌ์ ๋งฅ๋ฝ ์์ฝ ์ถ์ถ (์ต๊ทผ 3๊ฐ ๋ฉ์์ง)
|
| 317 |
+
normalized_history = self._normalize_history(history)
|
| 318 |
+
history_summary = "์์"
|
| 319 |
+
if normalized_history:
|
| 320 |
+
history_summary = "\n".join(
|
| 321 |
+
f"- {msg['role']}: {msg['content'][:150]}"
|
| 322 |
+
for msg in normalized_history[-3:]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
routing_prompt = (
|
| 326 |
+
"๋น์ ์ ๊ธ์ต/๊ธฐ์ ํธ๋ ๋ RAG ์์คํ
์ ์ง๋ฅํ ๋ผ์ฐํฐ์
๋๋ค.\n"
|
| 327 |
+
"์ฌ์ฉ์์ [ํ์ฌ ์ง๋ฌธ] ๋ฐ [์ต๊ทผ ๋ํ ํ์คํ ๋ฆฌ]๊ฐ ์๋ ์ ๊ณต๋ [๊ฒ์๋ ๋ด์ค ๋ฐ์ดํฐ]์ ์๋ฏธ์ ์ผ๋ก ๋ฐ์ ํ๊ฒ ์ฐ๊ด๋์ด ์๊ณ , "
|
| 328 |
+
"ํด๋น ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ง๋ฌธ์ ์ค์ ๊ตฌ์ฒด์ ์ด๊ณ ์ ๋ขฐํ ์ ์๋ ๋ต๋ณ์ ์ ๊ณตํ ์ ์๋์ง ํ๊ฐํ์ธ์.\n\n"
|
| 329 |
+
"ํนํ, ํ์ฌ ์ง๋ฌธ์ด '๊ทธ๊ฑฐ์ ๋ํด ์ข ๋ ์ค๋ช
ํด์ค'๋ '์์์ ํ์ ๋ ๋ค๋ฌ์ด์ค'์ ๊ฐ์ ํ์ ๋ํํ ์ง๋ฌธ์ผ ๊ฒฝ์ฐ, "
|
| 330 |
+
"[์ต๊ทผ ๋ํ ํ์คํ ๋ฆฌ]์ ๋ช
์๋ ์ฃผ์ ๊ธ์ต/๊ธฐ์ ํธ๋ ๋ ์ฃผ์ (์: ์ผ์ฑ์ ์ AI, ์นด์นด์ค AI ๋ฑ)๊ฐ "
|
| 331 |
+
"์๋ ๋ด์ค ๋ฐ์ดํฐ์ ํต์ฌ ๋ด์ฉ๊ณผ ์ผ์นํ๋์ง ์ข
ํฉ์ ์ผ๋ก ๊ณ ๋ คํด์ผ ํฉ๋๋ค.\n\n"
|
| 332 |
+
"๋ง์ฝ ์ง๋ฌธ ๋ฐ ๋ํ ๋งฅ๋ฝ์ด ์๋ ๋ด์ค ๋ฐ์ดํฐ์ ์ ํ ๋ฌด๊ดํ ์ผ๋ฐ ์์, ์ผ์์ ์ธ ๋ํ, ์ํ, ์์ ๋ฑ "
|
| 333 |
+
"์ง์ ๊ทธ๋ํ(๋ด์ค ๋ฐ์ดํฐ๋ฒ ์ด์ค)์ ์๋ ์ฃผ์ ์ ์ง๋ฌธ์ด๋ผ๋ฉด ๋ฐ๋์ 'NO'๋ผ๊ณ ๋ตํด์ผ ํฉ๋๋ค.\n"
|
| 334 |
+
"๋ด์ค ํฉํธ ๋ฐ์ดํฐ๋ฅผ ๊ฒฐํฉํ์ฌ ์ฌ๋ฐ๋ฅธ ๋ต๋ณ์ ์์ฑํ ์ ์๋ ๋งฅ๋ฝ์ด๋ผ๋ฉด 'YES', ๊ทธ๋ ์ง ์๋ค๋ฉด 'NO'๋ผ๊ณ ๋ง ๋ตํ์ธ์.\n\n"
|
| 335 |
+
f"[์ต๊ทผ ๋ํ ํ์คํ ๋ฆฌ]\n{history_summary}\n\n"
|
| 336 |
+
f"[ํ์ฌ ์ง๋ฌธ]\n{query_text}\n\n"
|
| 337 |
+
f"[๊ฒ์๋ ๋ด์ค ๋ฐ์ดํฐ]\n{context_snippet}\n\n"
|
| 338 |
+
"ํ์ (YES ๋๋ NO๋ก๋ง ๋ต๋ณ):"
|
| 339 |
+
)
|
| 340 |
+
# ์์ฃผ ๋น ๋ฅด๊ณ ์ ๋ ดํ ๋จ์ผ ํ ํฐ YES/NO ์๋ต ์์ฑ
|
| 341 |
+
response = self._rag_llm.invoke(
|
| 342 |
+
input=routing_prompt,
|
| 343 |
+
model_params={"temperature": 0, "max_tokens": 5}
|
| 344 |
+
)
|
| 345 |
+
decision = str(response.content).strip().upper()
|
| 346 |
+
return "YES" in decision
|
| 347 |
+
except Exception:
|
| 348 |
+
# ์์ธ ๋ฐ์ ์ ์์ ์ ์ํด ๊ธฐ์กด์ ๊ธฐ๋ณธ ๊ธธ์ด ๊ธฐ๋ฐ ํ์ ์ผ๋ก ํด๋ฐฑ
|
| 349 |
+
return len(total_content) >= 100
|
| 350 |
+
|
| 351 |
+
def _normalize_history(self, history: list) -> list:
|
| 352 |
+
"""Gradio ํ์คํ ๋ฆฌ(dict ๋๋ tuple ํ์)๋ฅผ LLM message_history ํ์์ผ๋ก ์ ๊ทํ"""
|
| 353 |
+
normalized: list = []
|
| 354 |
+
for msg in history:
|
| 355 |
+
if isinstance(msg, dict) and "role" in msg and "content" in msg:
|
| 356 |
+
normalized.append({"role": msg["role"], "content": str(msg["content"])})
|
| 357 |
+
elif isinstance(msg, (list, tuple)) and len(msg) == 2:
|
| 358 |
+
if msg[0]:
|
| 359 |
+
normalized.append({"role": "user", "content": str(msg[0])})
|
| 360 |
+
if msg[1]:
|
| 361 |
+
normalized.append({"role": "assistant", "content": str(msg[1])})
|
| 362 |
+
return normalized
|
| 363 |
+
|
| 364 |
+
def _generate_general_answer(self, query_text: str, history: list) -> str:
|
| 365 |
+
"""๊ทธ๋ํ ๊ฒ์ ๊ฒฐ๊ณผ ์์ด GPT-4o ์ผ๋ฐ ์ง์์ผ๋ก ๋ต๋ณ ์์ฑ (๋ํ ํ์คํ ๋ฆฌ ๋ฐ์)"""
|
| 366 |
+
assert self._rag_llm is not None
|
| 367 |
+
system_prompt = (
|
| 368 |
+
"๋น์ ์ AI ๋ฐ ํํ
ํฌ ๊ธฐ์ ํธ๋ ๋ ์ ๋ฌธ๊ฐ์ด์, ์ทจ์
์ค๋น์์ ์ญ๋ ๋ถ์์ ๋๋ ์ ๋ต ์ปจ์คํดํธ์
๋๋ค.\n"
|
| 369 |
+
"ํ์ฌ FinGraph ์ง์ ๊ทธ๋ํ(Neo4j GraphRAG)์์ ๊ด๋ จ ๋ด์ค ๊ธฐ์ฌ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n"
|
| 370 |
+
"์ด์ ๋ํ ๋งฅ๋ฝ์ ์ถฉ๋ถํ ๋ฐ์ํ๊ณ , GPT-4o์ ์ผ๋ฐ ํ์ต ๋ฐ์ดํฐ์ ๊ธฐ๋ฐํ์ฌ ์ต์ ์ ๋คํด ์ ๋ฌธ์ ์ผ๋ก ๋ต๋ณํด ์ฃผ์ธ์.\n\n"
|
| 371 |
+
"[์ค์ ์ง์นจ]\n"
|
| 372 |
+
"- ์ค์ ์กด์ฌํ์ง ์๋ ๋ด์ค ๋งํฌ, ๋ ์ง, ๊ฐ์ง URL์ ์ ๋ ์์ฑํ์ง ๋ง์ธ์.\n"
|
| 373 |
+
"- ๊ฐ๋ฅํ๋ค๋ฉด ์ทจ์
์ค๋น์์ด ๋ฉด์ /์์์์ ํ์ฉํ ์ ์๋ ์ค์ง์ ์ธ ์ธ์ฌ์ดํธ๋ฅผ ํฌํจํด ์ฃผ์ธ์.\n"
|
| 374 |
+
"- ๋ต๋ณ์ด ์ผ๋ฐ AI ํ์ต ๋ฐ์ดํฐ ๊ธฐ๋ฐ์์ ์จ๊ธฐ์ง ๋ง๊ณ ์์ฐ์ค๋ฝ๊ฒ ์ธ๊ธํ๋ฉฐ ์์ํ์ธ์."
|
| 375 |
+
)
|
| 376 |
+
normalized_history = self._normalize_history(history)
|
| 377 |
+
response = self._rag_llm.invoke(
|
| 378 |
+
input=query_text,
|
| 379 |
+
message_history=normalized_history,
|
| 380 |
+
system_instruction=system_prompt,
|
| 381 |
+
)
|
| 382 |
+
return str(response.content)
|
| 383 |
+
|
| 384 |
+
def search_with_fallback(self, query_text: str, history: list) -> HybridResult:
|
| 385 |
+
"""GraphRAG ๊ฒ์ -> ์ปจํ
์คํธ ํ์ง ํ๊ฐ -> ์ผ๋ฐ ์ง์ Fallback ํตํฉ ๋ฉ์๋.
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
query_text: ์ฌ์ฉ์ ์ง๋ฌธ ํ
์คํธ
|
| 389 |
+
history: ์ด์ ๋ํ ํ์คํ ๋ฆฌ (Gradio ํ์)
|
| 390 |
+
|
| 391 |
+
Returns:
|
| 392 |
+
HybridResult: ๋ต๋ณ, ๋ชจ๋("graph"|"general"), RetrieverResult
|
| 393 |
+
"""
|
| 394 |
+
self._init_once()
|
| 395 |
+
assert self._hybrid_retriever is not None
|
| 396 |
+
assert self._graphrag is not None
|
| 397 |
+
|
| 398 |
+
# 1๋จ๊ณ: LLM ํธ์ถ ์์ด DB ์ฟผ๋ฆฌ๋ง์ผ๋ก ๊ฒ์ ์คํ
|
| 399 |
+
retriever_result = self._hybrid_retriever.search(query_text=query_text)
|
| 400 |
+
|
| 401 |
+
# 2๋จ๊ณ: ์ปจํ
์คํธ ํ์ง ํ๊ฐ ํ ๋ผ์ฐํ
|
| 402 |
+
if self._is_context_sufficient(query_text, history, retriever_result):
|
| 403 |
+
# 3a. ๊ทธ๋ํ ๊ธฐ๋ฐ -> GraphRAG ๋ธ๋ฆฌํ ๋ต๋ณ ์์ฑ
|
| 404 |
+
rag_result = self._graphrag.search(query_text=query_text)
|
| 405 |
+
return HybridResult(
|
| 406 |
+
answer=rag_result.answer,
|
| 407 |
+
mode="graph",
|
| 408 |
+
retriever_result=rag_result.retriever_result,
|
| 409 |
+
)
|
| 410 |
+
else:
|
| 411 |
+
# 3b. ์ผ๋ฐ ์ง์ ๊ธฐ๋ฐ -> ํ๏ฟฝ๏ฟฝ๏ฟฝํ ๋ฆฌ ํฌํจ GPT-4o ์ง์ ํธ์ถ
|
| 412 |
+
answer = self._generate_general_answer(query_text, history)
|
| 413 |
+
return HybridResult(answer=answer, mode="general", retriever_result=None)
|
| 414 |
+
|
| 415 |
def search(self, *args: Any, **kwargs: Any) -> Any:
|
| 416 |
self._init_once()
|
| 417 |
assert self._graphrag is not None
|
src/utils/ui_templates.py
CHANGED
|
@@ -324,6 +324,12 @@ label.svelte-1ipelgc, span.svelte-1ipelgc {
|
|
| 324 |
display: none !important;
|
| 325 |
}
|
| 326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
/* โโ ์ฌ์ฉ์ ๋ฒ๋ธ โโ */
|
| 328 |
.message.user {
|
| 329 |
background: rgba(30,58,95,0.06) !important;
|
|
@@ -349,26 +355,32 @@ label.svelte-1ipelgc, span.svelte-1ipelgc {
|
|
| 349 |
background: transparent !important;
|
| 350 |
}
|
| 351 |
|
| 352 |
-
/* โโ ๋ฉ์์ง ๋ด๋ถ
|
| 353 |
.message p, .message li,
|
| 354 |
[class*="message"] p, [class*="message"] li {
|
| 355 |
-
line-height: 1.
|
| 356 |
-
margin
|
|
|
|
|
|
|
| 357 |
border: none !important;
|
| 358 |
border-left: none !important;
|
| 359 |
box-shadow: none !important;
|
| 360 |
background: transparent !important;
|
| 361 |
color: #1e3a5f !important;
|
| 362 |
}
|
|
|
|
|
|
|
|
|
|
| 363 |
.message blockquote, [class*="message"] blockquote {
|
| 364 |
border: none !important;
|
| 365 |
border-left: none !important;
|
| 366 |
padding: 0 !important;
|
|
|
|
| 367 |
background: transparent !important;
|
| 368 |
}
|
| 369 |
.message h3, [class*="message"] h3 {
|
| 370 |
-
margin-top:
|
| 371 |
-
margin-bottom:
|
| 372 |
font-weight: 800 !important;
|
| 373 |
color: #1e3a5f !important;
|
| 374 |
}
|
|
|
|
| 324 |
display: none !important;
|
| 325 |
}
|
| 326 |
|
| 327 |
+
/* โโ ๋ฉ์์ง ๋ฒ๋ธ ๊ธฐ๋ณธ ํฌ๊ธฐ ์ถ์ (์ฌ์ฉ์/๋ด ๊ณตํต ์ธ๋ก ๋์ด ์ต์ ํ) โโ */
|
| 328 |
+
.message {
|
| 329 |
+
padding: 10px 14px !important;
|
| 330 |
+
min-height: auto !important;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
/* โโ ์ฌ์ฉ์ ๋ฒ๋ธ โโ */
|
| 334 |
.message.user {
|
| 335 |
background: rgba(30,58,95,0.06) !important;
|
|
|
|
| 355 |
background: transparent !important;
|
| 356 |
}
|
| 357 |
|
| 358 |
+
/* โโ ๋ฉ์์ง ๋ด๋ถ ์ฌ๋ฐฑ ์์ ์ ์ด โโ */
|
| 359 |
.message p, .message li,
|
| 360 |
[class*="message"] p, [class*="message"] li {
|
| 361 |
+
line-height: 1.55 !important;
|
| 362 |
+
margin: 0 !important;
|
| 363 |
+
margin-bottom: 6px !important;
|
| 364 |
+
padding: 0 !important;
|
| 365 |
border: none !important;
|
| 366 |
border-left: none !important;
|
| 367 |
box-shadow: none !important;
|
| 368 |
background: transparent !important;
|
| 369 |
color: #1e3a5f !important;
|
| 370 |
}
|
| 371 |
+
.message p:last-child, .message li:last-child {
|
| 372 |
+
margin-bottom: 0 !important;
|
| 373 |
+
}
|
| 374 |
.message blockquote, [class*="message"] blockquote {
|
| 375 |
border: none !important;
|
| 376 |
border-left: none !important;
|
| 377 |
padding: 0 !important;
|
| 378 |
+
margin: 0 !important;
|
| 379 |
background: transparent !important;
|
| 380 |
}
|
| 381 |
.message h3, [class*="message"] h3 {
|
| 382 |
+
margin-top: 14px !important;
|
| 383 |
+
margin-bottom: 6px !important;
|
| 384 |
font-weight: 800 !important;
|
| 385 |
color: #1e3a5f !important;
|
| 386 |
}
|
tests/test_retrieval.py
CHANGED
|
@@ -41,3 +41,30 @@ def test_portfolio_showcase_aggregation_query():
|
|
| 41 |
assert any(indicator in answer for indicator in ["1.", "์ฒซ์งธ", "TOP", "๊ธฐ์ฌ", "์ถ์ฒ"])
|
| 42 |
|
| 43 |
print(f"\nโจ [ํฌํธํด๋ฆฌ์ค ์ผ์ผ์ด์ค RAG ๊ฒฐ๊ณผ]\n{answer}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
assert any(indicator in answer for indicator in ["1.", "์ฒซ์งธ", "TOP", "๊ธฐ์ฌ", "์ถ์ฒ"])
|
| 42 |
|
| 43 |
print(f"\nโจ [ํฌํธํด๋ฆฌ์ค ์ผ์ผ์ด์ค RAG ๊ฒฐ๊ณผ]\n{answer}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@pytest.mark.skipif(
|
| 47 |
+
not has_credentials,
|
| 48 |
+
reason="OpenAI API Key ๋๋ Neo4j ์ฐ๊ฒฐ ํ๊ฒฝ๋ณ์๊ฐ ์์ผ๋ฏ๋ก ํตํฉ ํ
์คํธ๋ฅผ ๊ฑด๋๋๋๋ค."
|
| 49 |
+
)
|
| 50 |
+
def test_hybrid_fallback_general_query():
|
| 51 |
+
"""
|
| 52 |
+
[ํ์ด๋ธ๋ฆฌ๋ RAG Fallback ์๋๋ฆฌ์ค]
|
| 53 |
+
์ง์ ๊ทธ๋ํ(๋ด์ค ๋ฐ์ดํฐ)์ ์ ํ ์์ง๋์ง ์์ ์ผ๋ฐ ๊ณผํ/์ญ์ฌ ์ง๋ฌธ์ ๋ํด
|
| 54 |
+
๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์๊ณ์น ๋ฏธ๋ง์์ ๊ฐ์งํ๊ณ ์๋์ผ๋ก GPT-4o ์ผ๋ฐ ์ง์ ๋ชจ๋(general)๋ก ๋ผ์ฐํ
ํ๋์ง ๊ฒ์ฆํฉ๋๋ค.
|
| 55 |
+
"""
|
| 56 |
+
general_query = "ํผํ๊ณ ๋ผ์ค ์ ๋ฆฌ์ ๊ทธ ์ค์ํ ํ์ฉ ์์๋ฅผ ๊ฐ๋จํ ์ค๋ช
ํด์ค."
|
| 57 |
+
|
| 58 |
+
# search_with_fallback์ ํตํ ๋ผ์ฐํ
๊ฒ์ ์ํ
|
| 59 |
+
result = graphrag.search_with_fallback(query_text=general_query, history=[])
|
| 60 |
+
|
| 61 |
+
# 1. ๋ฐํ ํ์
๋ฐ ๋ชจ๋ ๊ฒ์ฆ
|
| 62 |
+
assert result is not None
|
| 63 |
+
assert result.mode == "general"
|
| 64 |
+
|
| 65 |
+
# 2. GPT-4o ์ผ๋ฐ ์ง์ ๋ต๋ณ ์ ํจ์ฑ ๊ฒ์ฆ
|
| 66 |
+
assert len(result.answer.strip()) > 0
|
| 67 |
+
assert "ํผํ๊ณ ๋ผ์ค" in result.answer
|
| 68 |
+
|
| 69 |
+
print(f"\nโจ [์ผ๋ฐ ์ง์ Fallback ๋ผ์ฐํ
๊ฒฐ๊ณผ]\n{result.answer}")
|
| 70 |
+
|