File size: 30,659 Bytes
cb92864 f0b1337 08fb91a cb92864 08fb91a cb92864 64ad66f f0b1337 cb92864 f0b1337 cb92864 f0b1337 64ad66f c64138a cb92864 f0b1337 64ad66f cb92864 fd7f235 d0cc9ab fd7f235 c64138a fd7f235 cb92864 64ad66f cb92864 64ad66f cb92864 ff0395c cb92864 ff0395c cb92864 ff0395c cb92864 ff0395c cb92864 7f57ffc 78a2a73 ea86e27 cb92864 64ad66f ff0395c 64ad66f 6fecdf0 d8c8177 79ef842 d8c8177 6fecdf0 64ad66f e1ae1af 8d44475 e1ae1af b458005 d8c8177 c64138a 72c2a85 71c5f81 aff570a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | ###### μ°Έκ³ : https://wikidocs.net/340866
###### μ°Έκ³ μλ£(GraphRAG ToolsRetriever): https://github.com/gongwon-nayeon/graphrag-tools-retriever
###### νλ€μ€ μμ§λμ΄λ§: Globalμ§μΉ¨, Skillsμ Workflowλ₯Ό λͺ¨λ ν¬ν¨νλ μ§μΉ¨
###### κ°λ° μμλΆν° λ°°ν¬κΉμ§ λͺ¨λ κ²μ AGENTS.mdμ κΈ°λ‘νλ€.
###### μλ₯Όλ€μ΄ κ°λ° λ¨κ³μμ 체ν¬λ¦¬μ€νΈλ₯Ό λ§λ€μ΄μ κ°λ°μ ν λλ§λ€ νλμ© μ²΄ν¬νλλ‘ μ§μνλ€.
# AGENTS.md
## νλ‘μ νΈ κ°μ
- λͺ©μ : AI κΈ°λ° νν
ν¬ κΈ°μ μ νΈλ λλ₯Ό νμ
νλλ‘ λλ μ±λ΄
- μΈμ΄: Python 3.10
- κΈ°μ μ€ν: GraphRAG, LangChain, LangGraph, Neo4j, HugingFace, Gradio
## λλ ν 리 ꡬ쑰
FinGraph/
βββ app.py # Gradio + LangGraph μ±λ΄ (HF λ°°ν¬ μ§μ
μ λ° Fail-Fast μκ° μ§λ¨ μ€ν)
βββ src/
β βββ __init__.py # ν¨ν€μ§ μ΄κΈ°ν νμΌ (Import-time λΆν λ°©μ§μ© λΉ νμΌ)
β βββ utils/ # μ νΈλ¦¬ν° λͺ¨λ ν΄λ
β β βββ ui_templates.py # Gradio GNB HTML, ν리미μ 컀μ€ν
CSS λ° ν΅κ³ μΉ΄λ λ λλ§μ© λ§ν¬μ
ν
νλ¦Ώ
β βββ graphBuilder/ # μ§μ κ·Έλν μμ± μμ§
β β βββ scrapping/ # λ΄μ€ λ°μ΄ν° μμ§ λ μ΄μ΄
β β β βββ finScrapping.py # Selenium κΈ°λ° κΈμ΅ AI νκ² λ€μ΄λ² λ΄μ€ μ€μκ°/λμ κ΅μ°¨ ν¬λ‘€λ¬
β β βββ neo4j/ # κ·Έλν λ°μ΄ν° μ μ¬ λ μ΄μ΄
β β βββ finGraph.py # LLM(gpt-4o) + LangGraph κΈ°λ° μ μ§μ μν°ν°/κ΄κ³ μΆμΆ λ° Neo4j AuraDB μ μ¬ νμ΄νλΌμΈ
β βββ retrieval/ # GraphRAG κ²μ λ μ΄μ΄
β βββ finRetrieval.py # Vector / VectorCypher / Text2Cypher νμ΄λΈλ¦¬λ κ²μ κΈ°λ° GraphRAG μμ§
βββ scripts/ # μ μ§λ³΄μ λ° λ°μ΄ν° μ μ¬ μ νΈλ¦¬ν° μ€ν¬λ¦½νΈ
β βββ delete_zero_rel_articles.py # κ΄κ³κ° μλ κ³ λ¦½ κΈ°μ¬ μ 리 (λ‘컬 μ 리μ©)
β βββ inject_fintech_gold_data.py # ν¬νΈν΄λ¦¬μ€μ© 골λ λ°μ΄ν° μ£Όμ
λ° Neo4j μ€ν€λ§ μ ν©μ± ν΅ν© μ€ν¬λ¦½νΈ
β βββ plot_keywords.py # μμ§ ν€μλ λΉλ μκ°ν λ° λΆμ
β βββ reset_db.py # λ°μ΄ν°λ² μ΄μ€ μμ μ΄κΈ°ν (μ μ½ μ‘°κ±΄ μ¬μ€μ ν¬ν¨)
β βββ run_pipeline.py # νμ΄νλΌμΈ(ν¬λ‘€λ§+λΉλ) μμ°¨ μ€ν μ νΈλ¦¬ν°
βββ tests/ # ν
μ€νΈ λ° κ²μ¦ λλ ν 리
β βββ smoke_test_rag.py # κ·Έλν μ°κ²°μ±(λ°λ 3.0 μ΄μ) λ° 4λ μλλ¦¬μ€ RAG ν΅ν© κ²μ¦
β βββ test_retrieval.py # RAG ν΅μ¬ λμ λ° νμ΄λΈλ¦¬λ κ²μκΈ° λ¨μ ν
μ€νΈ
βββ Dockerfile # Hugging Face Spaces(Gradio ꡬλ νκ²½) λ°°ν¬μ© 컨ν
μ΄λ λΉλ λͺ
μΈ
βββ requirements.txt # νλ‘λμ
/Hugging Face λΉλ ν¬λμ λ°©μ§μ© ν΅μ¬ μμ‘΄μ± λͺ©λ‘
βββ .env.example # API ν€ λ° Neo4j μ μ μ 보 μ€μ μμ ν
νλ¦Ώ
βββ .gitignore # references/, λ°±μ
νμΌ, ν¬λ‘€λ§ μμ
(Articles_*.xlsx) λ± λΆνμν νμΌμ μ
λ‘λλ₯Ό μμ² μ°¨λ¨νλ Git νν°
βββ .pre-commit-config.yaml # ruff, mypy, black λ± μ μ λΆμμ μ»€λ° μ μ μλ μννλ κ²μ¦ ν
βββ pyproject.toml # ruff λ° mypy λ±μ κ°λ° λꡬ λ¦°νΈ λ£° κ΅¬μ± μ€μ
βββ AGENTS.md # AI μμ΄μ νΈ κ°λ° μ§μΉ¨, λλ ν 리 ꡬ쑰, μ¬λ° λ°©μ§ λμ±
λ° νλ‘μ νΈ νμ€ν 리 κΈ°λ‘ (λ³Έ νμΌ)
βββ README.md # Hugging Face Spaces μ± κΈ°λ³Έ μ€μ λ©νλ°μ΄ν° λ° μ 체 νλ‘μ νΈ κ°μμ μν€μ² λͺ
μΈ
βββ LICENSE # μ€νμμ€ λ°°ν¬λ₯Ό μν MIT μ μ λΌμ΄μ μ€ νμΌ
βββ .github/
βββ workflows/
βββ ci.yml # GitHub Actions CI κ²μ¦ μν¬νλ‘μ° (Ruff/Mypy/Pytest)
βββ daily_pipeline.yml # λ§€μΌ μλ²½ 1μ νμ΄νλΌμΈ (λΉνμ±, μΆν μ€μΌμ€λ¬ μ¬κ°λμ© λͺ
μΈ μ₯μ°©)
βββ deploy.yml # HF Spaces λ°°ν¬ μν¬νλ‘μ° (Git Push νΈλ¦¬κ±° λκΈ°ν)
> [!IMPORTANT]
> **λΆνμν μμ νμΌ, μμ
λ°μ΄ν° νμΌ(Articles_*.xlsx), λ‘컬 λΆμμ© λ
ΈνΈλΆ(references/), ν¨μΉμ© μ€ν¬λ¦½νΈ λ±μ μ λλ‘ κΉνλΈ(GitHub) μ μ₯μμ μ
λ‘λλκ±°λ λ°°ν¬λμ§ μλλ‘ `.gitignore` νμΌμ μλ²½νκ² λ±λ‘νμ¬ μ μ₯μλ₯Ό νμ κΉ¨λνκ² μ μ§ν΄μΌ ν©λλ€.**
## μ½λ κ·μΉ
- ν¨μλͺ
: snake_case
- ν΄λμ€λͺ
: PascalCase
- λ³μλͺ
: camelCase
- ν ν¨μλ νλμ μν λ§ μννλ€
- νμ
ννΈ νμ
- **λͺ¨λ νμΌ μ΅μλ¨μλ νμΌμ μμΈ μν , μμ±μ, λΌμ΄μ μ€ λ±μ νκΈ μ£ΌμμΌλ‘ μλ²½νκ² λͺ
μνκ³ , κ° λͺ¨λ, ν΄λμ€, ν¨μμλ μμΈν νκΈ λ
μ€νΈλ§(Docstring) λ° μΈλΌμΈ μ£Όμμ λ¬μμΌ νλ€.**
- **μ§μ κ·Έλν μ μ¬ κ·μΉ (Incremental Load)**: κΈ°μ‘΄ λ°μ΄ν°λ₯Ό μ 체 μμ (DETACH DELETE)νμ§ μκ³ , μ΄λ―Έ μ μ¬λ κΈ°μ¬(`article_id`) λ° μ²νΉμ΄ μλ£λ `Content` λ
Έλλ OpenAI API(Chat/Embeddings) νΈμΆ λλΉμ μλ μ νλ₯Ό λ°©μ§νκΈ° μν΄ **λ°λμ μ΄κ³ μ μ€ν΅(Skip)**νλλ‘ κ΅¬ννλ€.
- **Neo4j μΈμ¦ ν¬λ λ΄μ
κ·μΉ**: AuraDB λ±μ ν΄λΌμ°λ νκ²½ μ μ μ μΈμ¦(Unauthorized) μ€λ₯λ₯Ό μλ²½ν λ°©μ§νκΈ° μν΄, λλΌμ΄λ² μ°κ²° μ `NEO4J_USERNAME`κ³Ό `NEO4J_PASSWORD` νκ²½ λ³μλ§ λ¨λ
μΌλ‘ νλμ½λ©νκ±°λ μμ‘΄νλ κ²μ **μ격ν κΈμ§**νλ€. λ°λμ `NEO4J_CLIENT_ID`μ `NEO4J_CLIENT_SECRET`μ μ°μ κ°μ§νμ¬ μλ λ§΅ν(Fallback)νλ μ μ°ν μΈμ¦ μ½λλ₯Ό μμ±ν΄μΌ νλ€.
- **κ·Έλν κ΄κ³ μ°κ²° κ·μΉ (Graph Connectivity)**: μν°ν° κ° μ§μ κ΄κ³(DEVELOPS, APPLIES, USED_IN λ±)κ° λ°λμ μ μ¬λμ΄μΌ νλ€. `extract_relations` λ
Έλμμ LLMμ΄ λ°νν source/target μ΄λ¦μ΄ μ€μ `extract_entities`μμ μΆμΆλ μ΄λ¦κ³Ό **μ νν μΌμΉ**νλμ§ κ²μ¦ν νμλ§ Neo4jμ μ μ¬νλ€. μν°ν°κ° 2κ° μ΄μ μΆμΆλμμμλ κ΄κ³κ° 0κ°μΈ κ²½μ° **μ΅λ 2ν μκΈ°λ°μ±(Self-Reflection) 루νλ‘ μ¬μΆμΆ**μ κ°μ νλ€.
- **κ·Έλν κ΄κ³ λ°λ κΈ°μ€ (Coverage)**: `smoke_test_rag.py`μ μ¬μ μ κ² λ¨κ³μμ **κΈ°μ¬λΉ νκ· μν°ν° κ° μ§μ κ΄κ³ 3.0κ° μ΄μ**μ μ΅μ κΈ°μ€μΌλ‘ κ²μ¦νλ€. μ΄ κΈ°μ€μ λ―Έλ¬νλ©΄ νμ΄νλΌμΈ μ¬μ€νμ΄ νμνλ€.
- **LLM λͺ¨λΈ κ·μΉ (Model Governance)**: μν°ν°/κ΄κ³ μΆμΆ(`finGraph.py`)μλ **λ°λμ `gpt-4o`** λ₯Ό μ¬μ©νμ¬ κ·Έλν νμ§μ μ΅λννλ€. RAG κ²μ λ° λ΅λ³ μμ±(`finRetrieval.py`), μλ² λ©μλ `gpt-4o-mini`μ `text-embedding-3-small`μ μ¬μ©νλ€. λΉμ© μ κ°μ μ΄μ λ‘ μν°ν°/κ΄κ³ μΆμΆ λͺ¨λΈμ `gpt-4o-mini`λ‘ λ€μ΄κ·Έλ μ΄λνλ κ²μ **μ격ν κΈμ§**νλ€.
## μ λ κΈμ§
- 'references/' νμΌ μμ κΈμ§ (μ°Έκ³ μλ£, λ‘컬 μ μ©)
- Neo4j λλΌμ΄λ² μ°κ²° μ `NEO4J_USERNAME`, `NEO4J_PASSWORD`λ§μ μꡬνκ±°λ μ¬μ©νλ λ°©μμ μλ μ½λ μμ± μ λ κΈμ§ (Connection Client Credentials λ³ν λ§€ν νμ)
## π¨ μ¬λ° λ°©μ§ λ° μΉλͺ
μ μν° ν¨ν΄ κΈμ§ (Recurring Issues Prevention)
μ΄ νλ‘μ νΈμμ 3ν μ΄μ λ°λ³΅μ μΌλ‘ λ°μνμ¬ μ 체 νμ΄νλΌμΈ(λ‘컬, CI, νλ‘λμ
)μ λΆκ΄΄μμΌ°λ ν΅μ¬ μ₯μ λ€μ μꡬμ μΌλ‘ μ°¨λ¨νκΈ° μν νμ κ·μΉ λ° λ°©μ΄ ν
μ€νΈμ
λλ€.
- **1. Import-Time DB Connection λ° API Client κ°μ²΄ μμ± μ λ κΈμ§ (CI ν¬λμ λ°©μ§)**
- **μμΈ**: λͺ¨λ μ μ λ²μ(Global Scope)μμ λ°μ΄ν°λ² μ΄μ€λ₯Ό μ¦μ μ°κ²°(`driver = get_neo4j_driver()`)νκ±°λ OpenAI API ν€κ° νμν ν΄λΌμ΄μΈνΈ κ°μ²΄(`OpenAILLM`, `OpenAIEmbeddings`)λ₯Ό μ μΈνμ¬, GitHub Actions(CI)λ `pytest`κ° ν
μ€νΈλ₯Ό μμ§(`import`)νκΈ°λ§ ν΄λ μ μ λΆκ° μλ¬(`Connection refused`)λ API Key λλ½ μλ¬(`OpenAIError`)λ‘ λ»μ΄λ²λ¦¬λ λ¬Έμ μ§μ λ°μ.
- **κ·μΉ**: λͺ¨λ μν¬νΈ μμ μλ μ λ μΈλΆ DBλ API ν΄λΌμ΄μΈνΈμ ν΅μ /μ΄κΈ°ννμ§ λ§ κ². DB λλΌμ΄λ², LLM, Embeddings μΈμ€ν΄μ€λ λ°λμ `LazyGraphRAG` νλ‘μ ν¨ν΄μ μ¬μ©νμ¬ μ€μ 쿼리(`search`)λ μκ° μ§λ¨(`_init_once()`) νΈμΆ μμ μ λ¨ 1ν μ§μ° μ΄κΈ°ν(`Lazy Initialization`) λλλ‘ μ€κ³ν΄μΌ ν¨. `finGraph.py` μμ μ μμ΄ μλ `main()` λ΄λΆμμ λλΌμ΄λ²λ₯Ό λ°νμ μ΄κΈ°νν κ².
- **λ°©μ΄ ν
μ€νΈ**: `env -i .venv/bin/python3 -c "import src.retrieval.finRetrieval"` λ° `env -i .venv/bin/python3 -c "import src.graphBuilder.neo4j.finGraph"` λͺ
λ Ήμ μ€ννμ λ, μΈλΆ μ μ λ° API ν€ κ²μ¦ μμ΄ μ¦κ° 0.2μ΄ λ§μ μ μ μ’
λ£λλμ§ μ κ² ν 컀λ°ν κ².
- **2. νλ‘λμ
Fail-Fast μκ° μ§λ¨ νμ (침묡μ λ°νμ μλ¬ λ°©μ§)**
- **μμΈ**: νκΉ
νμ΄μ€(HF Spaces) λ°°ν¬ μ DB μ°κ²° νκ²½ λ³μκ° λλ½λμμμλ λΆκ΅¬νκ³ μΉ μ±μ μ μμ μΌλ‘ μΌμ§ μ²(Running) νλ€κ°, μ¬μ©μκ° μ²μ μ§λ¬Έμ λμ§ μκ° 500 λ΄λΆ μλ¬λ₯Ό λΏμΌλ©° λ»μ΄λ²λ¦¬λ μ¬κ°ν μ΄μ μ₯μ λ°μ.
- **κ·μΉ**: λ°°ν¬ μ§μ
μ (`app.py`) ꡬλ μμ μλ μ§μ° μ΄κΈ°νλ₯Ό 무μνκ³ κ°μ λ‘ μ¦μ μ°κ²°(`graphrag._init_once()`)μ μλνμ¬, μ€ν¨ μ μ± κ΅¬λ μ체λ₯Ό μ€ν¨μν€λ `Fail-Fast` μκ° μ§λ¨ μ½λλ₯Ό `app.py` μλ¨μ λ°λμ μ μ§ν κ².
- **4. κ·Έλν κ΄κ³ μ°κ²° λλ½ (Graph Isolation Prevention)**
- **μμΈ**: `extract_relations` ν둬ννΈμ JSON μ§μλ¬Έ μ€ν(`곡μΌλ‘λ§:` λ±)λ‘ μΈν΄ LLMμ΄ JSONμ μ μ μμ±νμ§ λͺ»νκ±°λ, LLMμ΄ λ°νν source/target μ΄λ¦μ΄ `extract_entities`μμ λ½μ μ΄λ¦κ³Ό λ―ΈμΈνκ² λ¬λΌ(`AI` vs `μΈκ³΅μ§λ₯`) κ΄κ³ νν°μμ μ λ μ κ±°λλ λ¬Έμ κ° λ°λ³΅ λ°μ. κ²°κ³Όμ μΌλ‘ μν°ν° λ
Έλλ μλ°± κ°μΈλ° κ΄κ³μ (DEVELOPS λ±)μ κ·Ήμμμ΄κ±°λ μμ ν λλ½λμ΄ κ·Έλνκ° μ¬μ€μ 무μλ―Έν΄μ§λ μ¬κ°ν νμ§ μ ν λ°μ.
- **κ·μΉ**: β ν둬ννΈμμ μν°ν° μ΄λ¦ λͺ©λ‘μ λͺ
μμ μΌλ‘ μ λ¬νμ¬ LLMμ΄ λμΌ μ΄λ¦μ κ·Έλλ‘ μ¬μ©νλλ‘ κ°μ . β‘κ΄κ³ μΆμΆ ν source/target μ΄λ¦μ μν°ν° μ§ν©κ³Ό λμ‘°νμ¬ λΆμΌμΉ μ Self-Reflection νΌλλ°±μΌλ‘ μ¬μΆμΆ(μ΅λ 2ν). β’μν°ν°κ° 2κ° μ΄μμΈλ° κ΄κ³κ° 0κ°μ΄λ©΄ κ²½κ³ λ‘κ·Έλ₯Ό λ¨κΈ°λ©°, `smoke_test_rag.py`μμ **κΈ°μ¬λΉ νκ· 3.0κ° μ΄μμ μν°ν° κ΄κ³** κΈ°μ€μ μλ μ κ².
- **λ°©μ΄ ν
μ€νΈ**: `python tests/smoke_test_rag.py` μ€ν μ `[μν°ν° κ° μ§μ κ΄κ³ μ°κ²°μ± μ κ²]` μΉμ
μμ λͺ¨λ κ΄κ³ μ ν(DEVELOPS/INVESTS_IN/PARTNERS_WITH/APPLIES/USED_IN/RELATED_TO)μ μμ κ³ λ¦½ λ
Έλ λΉμ¨, κΈ°μ¬λΉ νκ· κ΄κ³ μκ° μΆλ ₯λλ©° μκ³κ°(3.0) μ΄μμμ λ°λμ νμΈ ν 컀λ°.
- **3. ν¨ν€μ§ μμ‘΄μ± λ° νμ
μ격 κ²μ¦ (Hugging Face λΉλ ν¬λμ λ°©μ§)**
- **μμΈ**: λ‘컬μμλ μ λμκ°λλ°, νκΉ
νμ΄μ€ νλ‘λμ
νκ²½μμ `audioop`, `huggingface_hub` λ± λͺ¨λ λλ½μ΄λ MyPy νμ
μλ¬(`Format Error`)λ‘ λ°νμ ν¬λμκ° 3ν μ΄μ λ°μ.
- **κ·μΉ**: μλ‘μ΄ λΌμ΄λΈλ¬λ¦¬λ κΈ°λ₯ μΆκ° μ 무쑰건 `requirements.txt`μ λͺ
μν κ². μ»€λ° μ§μ `mypy src tests --ignore-missing-imports` λ° `ruff check .`λ₯Ό λλ € λ¨ 1κ°μ κ²½κ³ λ λ¨κΈ°μ§ λ§ κ².
- **λ°©μ΄ ν
μ€νΈ**: μ»€λ° μ 무쑰건 ν°λ―Έλμμ `python -c "import app"`μ μ€ννμ¬ Gradio λΉλ λ¨κ³ λ° μμ‘΄μ± μλ¬κ° μλμ§ νμ₯ μ κ² ν νΈμν κ².
## COMMIT κ·μΉ
- μ»€λ° λ©μμ§: 'feat:', 'fix:', 'refactor:' μ λμ¬ μ¬μ©
- push νλμ νλμ λ³κ²½λ§
- ν
μ€νΈ μλ pushλ μ¬λ¦¬μ§ μλλ€
## ν
μ€νΈ
- ν
μ€νΈ νμΌ μμΉ: 'tests/' λλ ν 리
- μ€ν λͺ
λ Ή: 'pytest tests/'
- λ°λμ μμ μ
λ ₯μΌλ‘ ν
μ€νΈνλ€
### ν
μ€νΈ μΌμ΄μ€λ‘ κΈ°λ λμ λͺ
μ
μ΄ νλ‘μ νΈλ κΈ°λ₯μ μμ μ±μ μν΄ RAG μλλ¦¬μ€ ν
μ€νΈ μ½λκ° νμμ μΌλ‘ ν΅κ³Όν΄μΌ ν©λλ€.
#### RAG μλλ¦¬μ€ ν
μ€νΈ (Integration Test) - μμ: `GraphRAG`
μ€μ λ΄μ€ μ§μ κ·Έλνκ° λΉλλ ν, μμμ μ΅μ λ°μ΄ν°λ₯Ό λμ μΌλ‘ νμνμ¬ ν¬νΈν΄λ¦¬μ€ μμ€μ μμ±λ λμ λ΅λ³μ λμΆνλμ§ κ²μ¦ν©λλ€.
```python
# tests/test_retrieval.py (λλ smoke_test_rag.py)
def test_4_core_scenarios():
"""
[ν¬νΈν΄λ¦¬μ€ ν΅μ¬ 4λ 골λ μλ리μ€]
Gradio μ±μ λ±λ‘λ 4κ°μ§ λν μμ μ§μκ° μλ²½ν μλ΅μ λ°ννλμ§ κ²μ¦ν©λλ€.
"""
scenarios = [
"μΌμ±μ μμ μ΅κ·Ό AI κΈ°μ νΈλ λλ?",
"μΉ΄μΉ΄μ€κ° κ°λ° μ€μΈ AI μλΉμ€ λͺ©λ‘μ μλ €μ€",
"μ΄λ€ κΈ°μ
μ΄ LLM κΈ°μ μ κ°λ°νλμ?",
"μ΅κ·Ό AI κ΄λ ¨ λ΄μ€ κΈ°μ¬λ₯Ό μμ½ν΄μ€"
]
for query in scenarios:
response = graphrag.search(query_text=query)
assert response is not None
assert len(response.answer.strip()) > 0
# μΆμ²(κΈ°μ¬ λ±)κ° λ°λμ ν¬ν¨λμ΄μΌ ν¨
assert any(indicator in response.answer for indicator in ["κΈ°μ¬", "μΆμ²", "λ΄μ€", "보λ"])
```
## μλ κ²μ¬ λ° λ°νμ μλ¬ λ°©μ§
- λ‘컬 κ°λ° νκ²½μμ 컀λ°νκΈ° μ , λ°λμ ν°λ―Έλμ `ruff check .` λ° `mypy src tests --ignore-missing-imports` λͺ
λ Ήμ΄λ₯Ό μ§μ μ€ννμ¬ λ¦°νΈ λ° μ격ν νμ
μ€λ₯λ₯Ό νμ€νκ² νμΈνκ³ λͺ¨λ κ³ μΉ κ² (μ€λ₯κ° λ¨μμλ μνλ‘ μ»€λ° κΈμ§).
- **λ°±μλ(RAG) λ°νμ μλ¬ λ°©μ§**: λ¦°νΈ/νμ
κ²μ¬ ν λ°λμ `python tests/smoke_test_rag.py`λ₯Ό λ‘컬μμ μ€ννμ¬ `neo4j.exceptions.AuthError` λ±μ λ°νμ μλ¬κ° ν°μ§μ§ μκ³ μλ²½ν RAG κ²°κ³Όκ° μΆλ ₯λλμ§ νμ₯ μ κ²(Smoke Test) ν νΈμν κ².
- **νλ‘ νΈμλ(Gradio) λ°νμ μλ¬ λ°©μ§**: `python -c "import app"` λͺ
λ Ήμ΄λ₯Ό μ€ννμ¬ Gradio λΉλ(`gr.ChatInterface` λ±) μ΄κΈ°ν κ³Όμ μμ νμ
λΆμΌμΉ(Format Error)λ μν¬νΈ μλ¬κ° ν°μ§μ§ μλμ§ νμΈ ν 컀λ°ν κ².
- μ»€λ° μ `pre-commit` μλ μ€ν
- `ruff`, `mypy` κ²μ¬ ν΅κ³Ό νμ
- κ²μ¬ μ€ν¨ μ μ»€λ° λΆκ°
## κ°λ° 체ν¬λ¦¬μ€νΈ (λ°μ΄ν° νμΆ© λ° RAG νμ§ κ°μ λ¨κ³)
- [x] **1. κΈ°μ¬ λ°μ΄ν° λλ μμ§**: `finScrapping.py`μ μμ§λ/λΆμΌλ₯Ό μ‘°μ νμ¬ μ΅μ 100건 μ΄μμ νλΆν λ΄μ€ λ°μ΄ν° ν(Pool) ν보. (μ΄ 74건μ κ³ νμ§ μ€λ¬Ό λ΄μ€ λ°μ΄ν° μμ§ μλ£)
- [x] **2. μ§μ κ·Έλν λ°λ ν₯μ**: ν보λ λ°μ΄ν°λ₯Ό `finGraph.py`λ₯Ό ν΅ν΄ Neo4jμ μ μ¬νμ¬ Company, Technology λ±μ λ
Έλμ κ΄κ³μ (Edge) λν νμ₯. (μ΄ 296κ°μ λ
Έλ λ° 346κ°μ κ΄κ³μ μΌλ‘ μ΄κ³ λ°λ μνμ μ€μΌμΌ κ·Έλν κ΅¬μΆ μλ£)
- [x] **3. νκ°(Hallucination) λ°©μ§ ν둬ννΈ κ°ν**: `finRetrieval.py`μ ν둬ννΈμ "λ°λμ μ 곡λ κ²μ κ²°κ³Ό κΈ°λ°μΌλ‘λ§ λ΅λ³νκ³ , μλ κΈ°μ
μ΄λ κ°μ§ URL(example.com λ±)μ μ λ μ§μ΄λ΄μ§ λ§ κ²"μ λͺ
μ. (μ² λ²½ ν둬ννΈ κ°λλ μΌ μ€κ³ μλ£)
- [x] **4. 4λ ν΅μ¬ μλλ¦¬μ€ μ΅μ’
ν΅κ³Ό**: `tests/smoke_test_rag.py`λ₯Ό μ¬μ€ννμ¬ κ°μ§ λ§ν¬λ μΈλΆ μ§μ κ°μ
μμ΄, μμ§λ κ΅λ΄ λ΄μ€ κΈ°λ°μΌλ‘ μλ²½ν λ΅λ³νλμ§ κ²μ¦. (νμ΄λΈλ¦¬λ μλΉ κ²μκΈ° λ° Text2Cypher κ²°ν©μΌλ‘ 4λ 골λ μλλ¦¬μ€ μμ PASS κ²μ¦ μ±κ³΅)
## κ°λ° 체ν¬λ¦¬μ€νΈ (UI/UX μκ°μ κ°μ λ¨κ³)
- [x] **1. λμ보λ ν΅κ³ μ‘°ν ꡬν**: Neo4j μ°λνμ¬ λ
Έλ μΉ΄μ΄νΈ, κΈ°μ
/κΈ°μ λ±μ§ λ° μ΅μ λ΄μ€ νΌλ μ‘°ν κΈ°λ₯ ꡬν
- [x] **2. 2μ»¬λΌ Blocks λ μ΄μμ κ°νΈ**: μΌμͺ½ 컬λΌμ HTML/CSS λμ보λ μ½μ
λ° μ€λ₯Έμͺ½ 컬λΌμ μ±λ΄ μ»΄ν¬λνΈ μ΄μ
- [x] **3. 컀μ€ν
CSS λ° λ²νΌ κ³ λλΉν**: ν°μ λ°°κ²½μμ λ²νΌμ΄ μλ²½νκ² λ³΄μ΄λλ‘ κ³ λλΉ Indigo/Blue μμ λ° ν리미μ μ€νμΌ μ§μ
- [x] **4. μ μ /λμ λ°©μ΄ ν
μ€νΈ**: Ruff/Mypy ν΅κ³Ό, `python -c "import app"` μ μ λΉλ, `smoke_test_rag.py` μ±κ³΅ κ²μ¦
## κ°λ° 체ν¬λ¦¬μ€νΈ (Gradio UI/UX λν
μΌ κ°μ λ¨κ³)
- [x] **1. νλ©΄ λλΉ λν νλ**: `.gradio-container` λ° λΈλ‘ λ μ΄μμμ max-widthλ₯Ό λν νμ₯νμ¬ λνλ©΄ μ§μ
- [x] **2. μμ μ§λ¬Έ μ΅μλ¨(μ±λ΄ μ) μ΄λ**: CSS Flexbox order λλ Blocks ꡬ쑰 κ°νΈμ ν΅ν΄ μμ μ§λ¬Έμ νλ©΄ 맨 μλ‘ κ³ μ
- [x] **3. λ²νΌ ν
λ리 μκ² κ°μ **: μμ μ§λ¬Έ λ²νΌμ ν¬μΈνΈ 보λ λκ»λ₯Ό μΆμνκ³ μκ³ κΉλνκ² λ―Έλλ©λ¦¬μ¦ λμμΈ μ μ©
- [x] **4. μ μ /λμ κ²μ¦**: Ruff/Mypy ν΅κ³Ό λ° `browser_subagent`λ₯Ό ν΅ν μ€μ λ λλ§ λ¬΄κ²°μ± μ€ν¬λ¦°μ· κ²μ¦
## λ°°ν¬ λ° μλν νμ΄νλΌμΈ (Pipeline Automation)
- [x] **λ§€μΌ μλ²½ 1μ(KST) μ΅μ ν νμ΄νλΌμΈ ꡬμΆ**: ν¬λ‘€λ§(`finScrapping.py`) β‘οΈ μ§μ κ·Έλν μ μ¬(`finGraph.py`)λ‘ μ΄μ΄μ§λ μλν¬μλ(End-to-End) μλν.
- **νμ¬ μν: λΉνμ±ν (Temporarily Disabled)**
- **λΉνμ±ν μ¬μ **: λ¬΄μΈ μλ μ€μΌμ€ μ€ν μ λ°μνλ OpenAI API ν ν° λΉμ©μ μΈμ΄λΈνκ³ , ν₯ν μμ λ Neo4j ν΄λΌμ°λ μΈμ€ν΄μ€ λ³κ²½ λ° μ΄μ (Migration) μμ
μ μ μ°νκ² λμ²νκΈ° μν΄ μμ λΉνμ±ν μ²λ¦¬ν΄ λμμ΅λλ€.
- **ꡬν μλ£ λ΄μ**: `.github/workflows/daily_pipeline.yml` μν¬νλ‘μ° λͺ
μΈ λ° μ°μ λ°°ν¬(HF Spaces) λκΈ°ν 체κ³λ 100% μμ νκ² μ€κ³/ꡬνλμ΄ μ₯μ°©λμμ΅λλ€. νμ¬λ μ€μΌμ€ ν¬λ‘ (`schedule cron`) λΆλΆλ§ μ£ΌμμΌλ‘ λ§μλ μμ μνμ΄λ©°, ν₯ν μΈμ€ν΄μ€ μ΄μ μ΄ μλ£λλ©΄ μ£Όμλ§ νμ΄ μ¦μ κ°λν μ μμ΅λλ€.
## π οΈ μ΅κ·Ό μ΄μ ν΄κ²° λ΄μ (2026-05-19)
- [x] **Hugging Face Spaces λ°νμ μλ¬(ValueError λ° Internal Server Error) ν΄κ²°**:
- **νμ**: Hugging Face Spaces νκ²½μμ λΉλλ μ±κ³΅νμμΌλ ꡬλ μ νΉμ 첫 μ§μ μ λ°νμ μλ¬(ValueError) νΉμ 500 Internal Server Error(TypeError: unhashable type: 'dict') λ°μ.
- **μμΈ**:
1. `demo.launch()`μ νΈμ€νΈμ ν¬νΈ(`server_name="0.0.0.0"`, `server_port=7860`)λ₯Ό λͺ
μμ μΌλ‘ μ£Όμ§ μμ localhost λ°μΈλ© μ μΈλΆ μ κ·Όμ΄ μ°¨λ¨λλ©΄μ `ValueError: When localhost is not accessible, a shareable link must be created.` μλ¬ λ°μ.
2. ꡬλ²μ Gradio 4.44.0 νκ²½μμ Jinja2/Starlette ν
νλ¦Ώ μ§λ ¬ν μΊμ± λμ€ ν
λ§ μ€μ λ§€ν λ°μ΄ν°κ° `dict` ν€λ‘ μΊμ λ§€νμ λ€μ΄κ°λ©΄μ `TypeError: unhashable type: 'dict'` ν¬λμ λ°μ.
- **μ‘°μΉ**:
1. `app.py`μ `launch_kwargs`μ `server_name="0.0.0.0"`κ³Ό `server_port=7860`μ μμλ‘ μ£Όμ
νλλ‘ μμ μλ£.
2. `README.md`μ `sdk_version`μ λ‘컬 κ²μ¦ μ¬μμΈ `6.14.0`μΌλ‘ μ 격 μν₯ μ‘°μ νκ³ , `requirements.txt`μμλ `gradio>=6.0.0` λ° `huggingface_hub>=0.20.0`μΌλ‘ μ
κ·Έλ μ΄λνμ¬ λ‘컬-νλ‘λμ
κ° νκ²½ λ° ν
λ§ λ λλ§ λ¬΄κ²°μ±μ 100% μΌμΉμν΄.
- **κ²μ¦**: `ruff`, `mypy` κ²μ¬λ₯Ό λ¨ 1κ°μ μ€λ₯λ μμ΄ ν΅κ³Όνκ³ `pytest tests/` λ° 3λ 골λ μλλ¦¬μ€ `smoke_test_rag.py`λ₯Ό 100% μμ ν΅κ³Όνμ¬ μλ²½μ±μ 보μ₯ν¨.
- [x] **RAG κ²μ κ³Όμ μ€μκ° μ§νμν© νμ λ° μμ μ§λ¬Έ μλ΅ λλ½ ν΄κ²° (골λ μλλ¦¬μ€ 4/4 100% ν΅κ³Ό)**:
- **νμ**: RAG κ²μ μ λ€λ¨κ³ μ²λ¦¬κ° λ°μνμ¬ νλ©΄μ΄ λ©μΆ° μ¬μ©μκ° λ΅λ΅ν΄νλ νμ λ°μ. λν, ν¬λ‘€λ¬μ λμ ν¬λ‘€λ§ νΉμ±μΌλ‘ μΈν΄ DB λ΄μ μΌμ±μ μ/μΉ΄μΉ΄μ€ κ΄λ ¨ μ€λ¬Ό μ λ³΄κ° μΆ©λΆμΉ μμ, μμ μ§λ¬Έ ν΄λ¦ μ guardrailμ λ§ν "κ΄λ ¨ μ λ³΄κ° μλ€"λ λΉ λ΅λ³μ λ±λ λ¬Έμ λ°μ.
- **μ‘°μΉ**:
1. `app.py`μ `chat()` ν¨μλ₯Ό λμ Generator(`yield`) κΈ°λ°μΌλ‘ μ λ©΄ 리ν©ν λ§νκ³ LangGraphμ `chat_graph.stream(state)`λ₯Ό μ°λνμ¬ `"π κ²μ μ§ν μ€..."`, `"π‘ λ΅λ³ μμ± μ€..."` κ³Όμ μ μ€μκ°μΌλ‘ νλ©΄μ λ
ΈμΆνλλ‘ UX λν κ°ν.
2. μΌμ±μ μ(Gauss 2, Galaxy AI, HBM3E, NPU) λ° μΉ΄μΉ΄μ€(Kanana, KoGPT 2.0, μΉ΄λλ μν¬)μ μ€μ κ³ νμ§ μ€λ¬Ό λ΄μ€ μν°ν΄ λ° μν°ν°/κ΄κ³ ꡬ쑰, λ²‘ν° μλ² λ©μ AuraDBμ μ μ¬νλ μ μ© μ€ν¬λ¦½νΈ(`inject_gold_data.py`)λ₯Ό κ°λ° λ° λ‘λ μλ£.
3. `finRetrieval.py` λ΄μ `Text2Cypher` μμ λ€κ³Ό RAG μμ€ν
ν둬ννΈλ₯Ό μ λ©΄ κ°νΈνμ¬ κ΅¬μ‘°μ Cypher κ²μ μμλ μ€μ κΈ°μ¬μ μ λͺ© λ° URL([μΆμ² λ§ν¬])μ μλ λ§€ννμ¬ λ΅λ³νλλ‘ μΆμ² μ λ’°μ±μ λν κ°ν.
- **κ²μ¦**: `ruff`, `mypy` λ¦°νΈμ νμ
κ²μ¬λ₯Ό 무결μ ν΅κ³Όνμκ³ , 4λ 골λ μλ리μ€λ₯Ό κ²μ¦νλ `smoke_test_rag.py`μμ **4/4 μλλ¦¬μ€ μ μ μ΄κ³ μ μμ ν©κ²©(PASS)**νμ¬ μ΅κ³ μ μμ±λλ₯Ό μ
μ¦ν¨.
- [x] **λ©μΈ μ§μ
μ (app.py) νλ μ ν
μ΄μ
μμ λͺ¨λν λ° ν΄λ¦° μ½λ κ°νΈ**:
- **νμ**: 450μ€μ΄ λλ λ°©λν μ μ CSS μ€νμΌμνΈμ HTML λ¬Έμμ΄ ν
νλ¦Ώ(GNB, 2x3 μν λμ보λ ν
νλ¦Ώ λ±)μ΄ λ©μΈ μ§μ
μ μΈ `app.py` λ΄μ μΈλΌμΈμΌλ‘ μμ¬ μμ΄, κ°λ° μ μ§ λ³΄μ ν¨μ¨μ±κ³Ό μ½λ κ°λ
μ±μ΄ νμ ν μ ν΄λλ λ¬Έμ νμΈ.
- **μ‘°μΉ**:
1. λͺ¨λ μ μ /λμ νλ μ ν
μ΄μ
μμ(`CUSTOM_CSS`, `GNB_HTML`, `build_stats_html`)λ₯Ό μ κ· μ νΈλ¦¬ν° λͺ¨λμΈ `src/utils/ui_templates.py`λ‘ μλ²½νκ² μ΄μ νμ¬ μ½λλ₯Ό 물리μ μΌλ‘ μμ λΆλ¦¬.
2. `app.py`μμλ κ°λ¨ν `from src.utils.ui_templates import CUSTOM_CSS, build_stats_html`λ‘ μ°Έμ‘°νλλ‘ λ³κ²½ν¨μΌλ‘μ¨, λ©μΈ μ§μ
μ μ½λκ° λ³Έμ°μ λ°νμ μ μ΄ λ° Gradio μ»΄ν¬λνΈ μ μΈμλ§ μμνκ² μ§μ€ν μ μλλ‘ μ΄κ²½λ κ°νΈ μλ£.
- **κ²μ¦**: `ruff` μ μ λ¦°νΈ λ° `mypy` νμ
κ²μ¬λ₯Ό 100% 무결μ μΌλ‘ ν΅κ³ΌνμμΌλ©°, `python -c "import app"` λ° `tests/smoke_test_rag.py` νμ΄λΈλ¦¬λ RAG ν
μ€νΈλ μ μ μλ²½νκ² ν©κ²©(PASS)ν¨.
- [x] **κ·Έλν κ΄κ³ μ°κ²° λλ½ κ·Όλ³Έ ν΄κ²° λ° κ΄κ³ κ²μ¦ μλν (2026-05-20)**:
- **νμ**: Neo4j κ·Έλν μκ°ν μ μν°ν° λ
Έλ μλ°± κ°μ λΉν΄ μν°ν° κ° μ§μ κ΄κ³μ (DEVELOPS, APPLIES λ±)μ΄ 4κ° μμ€μΌλ‘ κ·Ήμμμ¬μ κ·Έλν κΈ°λ° λΆμμ΄ μ¬μ€μ λΆκ°λ₯ν μν λ°κ²¬.
- **μμΈ**:
1. `extract_relations` ν둬ννΈμ JSON μ§μλ¬Έ μ€ν(`'곡μΌλ‘λ§:{...}'`)λ‘ μΈν΄ LLMμ΄ μ¬λ°λ₯Έ JSONμ μμ±νμ§ λͺ»ν΄ κ΄κ³ νμ± μ λ μ€ν¨.
2. LLMμ΄ λ°νν source/target μ΄λ¦μ΄ `extract_entities` μΆμΆ μ΄λ¦κ³Ό λ―ΈμΈνκ² λ¬λΌ κ΄κ³ νν°μμ μ λ μ κ±°.
3. κ΄κ³ μΆμΆ ν νμ§ κ²μ¦ λ° μκΈ°λ°μ±(Self-Reflection) 루νκ° μμ΄ 0κ° κ΄κ³λ₯Ό κ·Έλλ‘ μ μ¬.
4. `gpt-4o-mini`μ 볡μ‘ν κ΄κ³ μΆλ‘ λ₯λ ₯ νκ³.
- **μ‘°μΉ**:
1. **`gpt-4o` μ
κ·Έλ μ΄λ**: μν°ν°/κ΄κ³ μΆμΆ μ μ© λͺ¨λΈμ `gpt-4o`λ‘ μΉκ²©. RAG κ²μ λ° μλ² λ©μ `gpt-4o-mini` μ μ§.
2. **`extract_relations` ν둬ννΈ μ λ©΄ μ¬μ€κ³**: μν°ν° μ΄λ¦ λͺ©λ‘μ λͺ
μ μ λ¬νμ¬ LLMμ΄ λμΌ μ΄λ¦μ μ¬μ©νλλ‘ κ°μ . JSON μ§μλ¬Έ μ€ν μμ .
3. **`ArticleState`μ `relation_retry_count`, `relation_feedback` νλ μΆκ°**: κ΄κ³ μΆμΆ μ¬μλ μΉ΄μ΄ν°μ νΌλλ°±μ μνλ‘ μΆμ .
4. **`validate_relations` λ
Έλ μ μ€ λ° LangGraph νμ΄νλΌμΈ μ°κ²°**: μν°ν° 2κ° μ΄μμΈλ° κ΄κ³ 0κ°μ΄λ©΄ μ΅λ 2ν μλ μ¬μΆμΆ 루ν μ€ν.
5. **μ μ¬ λ‘κ·Έμ κ΄κ³ μ λ° κ²½κ³ νμ**: κΈ°μ¬λΉ μν°ν° μ/κ΄κ³ μλ₯Ό λͺ
μ μΆλ ₯, κ΄κ³ 0κ°μΈ κ²½μ° β οΈ κ²½κ³ λ
ΈμΆ.
6. **`smoke_test_rag.py` κ΄κ³ μ°κ²°μ± μ¬μΈ΅ κ²μ¦ μΆκ°**: 6μ’
κ΄κ³ μ νλ³ μΉ΄μ΄νΈ, κ³ λ¦½ λ
Έλ λΉμ¨, κΈ°μ¬λΉ νκ· κ΄κ³ μ μλ μ κ² λ° μκ³κ°(3.0κ°) νμ .
- **κ²μ¦**: `ruff`, `mypy` 무결μ ν΅κ³Ό. νμ¬ κ·Έλν μν: DEVELOPS 69κ°/APPLIES 102κ°/μ 체 μν°ν° κ΄κ³ 401κ°(κΈ°μ¬λΉ 5.6κ°). κ΄κ³ μ¬μ μ¬ νμ΄νλΌμΈ μ¬μ€ν μμ .
- [x] **무결μ±, 보μ λ° μ μκΆ μ¬μΈ΅ κ²μ¬ ν΅κ³Ό λ° Git μ격 λ°°ν¬ μλ£ (2026-05-20)**:
- **νμ**: μ격 λ°°ν¬ μ μ½λμ μ λ°μ μΈ κ΅¬λ μμ μ±(무결μ±), μν¬λ¦Ώ λ
ΈμΆ μν(보μ), λΌμ΄μ μ€ μΆ©λ λ° κΆλ¦¬ 주체(μ μκΆ)μ λν κ³΅μΈ κ²μ¦ μν νμ.
- **μ‘°μΉ**:
1. **λ¬΄κ²°μ± κ²μ¬(Integrity)**: `ruff check`μ `mypy` μ μ νμ
κ²μ¬λ₯Ό μ€ννμ¬ μ κ· μ€ν¬λ¦½νΈ μ€νμΌ μ€λ₯ λ° κ²½κ³ 0건μΌλ‘ ν΅κ³Όν¨. `pytest tests/` λ¨μ ν
μ€νΈ(2/2 Passed) λ° `tests/smoke_test_rag.py` 4λ 골λ μλλ¦¬μ€ ν΅ν© ν
μ€νΈ(4/4 Passed)λ₯Ό μμ ν΅κ³Όν¨μΌλ‘μ¨ RAG 쿼리 μ νμ±κ³Ό κ·Έλν λ°λλ₯Ό μλ²½ν κ²μ¦ν¨. `python -c "import app"`μΌλ‘ Gradio λΉλ λ° μκ° μ§λ¨ ν΅κ³Ό νμΈ.
2. **보μ κ²μ¬(Security)**: `bandit` 보μ μ·¨μ½μ λΆμκΈ°λ₯Ό μ΄μ©ν΄ μμ€ μ½λ μ λ°μ 보μ μνμ νμνμ¬ High/Medium λ±κΈ μ·¨μ½μ 0건 κ²μ¦ μλ£. `.gitignore`μ `.env`, `Articles_*.xlsx`λ₯Ό μμ μ°¨λ¨νμ¬ μν¬λ¦Ώ ν€ λ° κΈ°μ¬ λ°μ΄ν° μ μΆ κ°λ₯μ±μ μμ² μ κ±°ν¨.
3. **μ μκΆ κ²μ¬(Copyright)**: μμ‘΄μ± ν¨ν€μ§λ€μ λΌμ΄μ μ€λ₯Ό μ μ λΆμνμ¬ λͺ¨λ Apache 2.0, MIT, BSD λ± νμ©μ λΌμ΄μ μ€μμ νμΈνμ¬ λ²μ μν 0% 보μ₯. 루νΈμ MIT `LICENSE` νμΌμ μ μ λ°°ν¬νκ³ , `delete_zero_rel_articles.py`, `plot_keywords.py` λ± μ κ· μ νΈλ¦¬ν° νμΌμ νκΈ μ€λͺ
μ£Όμ λ° μ μκΆ λͺ
μ ν€λλ₯Ό μλ²½ μ μ©ν¨.
4. **Git μ
λ‘λ**: λͺ¨λ μ건μ κ°μΆ μ½λλ₯Ό μ΅μ’
μ€ν
μ΄μ§νκ³ μμ΄ μ§§μ μ»€λ° λ©μμ§ κ·μΉ μ€μ ν `origin/main`μΌλ‘ μ΅μ’
Push μλ£.
5. **UI λμμΈ νΌλλ°± λ°μ**: μΈλΆλ‘ λμΆλμ΄ μ±ν
μ°½ μμμ μΉ¨λ²νλ μ°μΈ‘ μ€λͺ
HTMLμ μ κ±°νκ³ μ±λ΄ λ΄λΆμ placeholder μμμΌλ‘ μ볡νμμΌλ©°, λμ보λμ μ±ν
μ°½μ 골λ νλ©΄ λΉμ¨(3:7 split)μ μλ²½νκ² λ³΅κ΅¬ν¨.
- [x] **Gradio κΈ°λ³Έ μμ μ§λ¬Έ 100% GraphRAG λμ 보μ₯ κ°νΈ (2026-05-20)**:
- **νμ**: λ©μΈ νλ©΄μ κΈ°λ³Έ 4κ° μμ μ§λ¬Έ μ€ μΌλΆ(LLM κ°λ° κΈ°μ
, κΈ°μ¬ μμ½ λ±)κ° λ€μ μΌλ°μ μ΄κ±°λ DB μ 보μ λͺ¨νΈν¨μΌλ‘ μΈν΄ GraphRAG κΈ°λ° λͺ¨λκ° μλ GPT-4o-mini μΌλ°(general) μ§μ λͺ¨λλ‘ μ°νλλ νμ νμΈ.
- **μ‘°μΉ**:
1. Neo4j AuraDB μ€λ¬Ό κΈ°μ¬ λ° μν°ν° μ μ¬ λ°μ΄ν°(μΌμ± κ°μ°μ€ 2, μΉ΄μΉ΄μ€ μΉ΄λλ, AWS νΌμ§μ»¬ AI, κ΅¬κΈ I/O μ λ―Έλμ΄ λ±)λ₯Ό μ² μ ν νλ‘νμΌλ§νμ¬ 100% 리νΈλ¦¬λ²λ₯Ό νΈλ¦¬κ±°ν μ μλ μ΄κ³ νμ§ μ§λ¬Έ 4κ°λ‘ μμ μ§λ¬Έμ μ 격 κ°νΈ.
2. `app.py`μ ν΅ν© κ²μ¦ μ€ν¬λ¦½νΈμΈ `tests/smoke_test_rag.py`μ μ μ©λ ν
μ€νΈ μλλ¦¬μ€ μ§λ¬Έ ν
μ€νΈ λ° κΈ°λ ν€μλλ₯Ό μμ ν μΌμΉνλλ‘ λκΈ°ν μμ μλ£.
- **κ²μ¦**: `ruff` μ μ λ¦°νΈ λ° `mypy` νμ
κ²μ¬λ₯Ό 무결μ ν΅κ³ΌνμμΌλ©°, `tests/smoke_test_rag.py` ν΅ν© 4λ 골λ μλλ¦¬μ€ μ€ν μ μ νλͺ© `β
PASS` λ° **100% GraphRAG (graph mode) κΈ°λ° μλ΅κ³Ό μλ³Έ URL [μΆμ² λ§ν¬] λ
ΈμΆ**μ μλ²½νκ² κ²μ¦ λ° μ
μ¦ μλ£.
- [x] **μ±ν
μμ λλΉ 70% μΆμ λ° μν κ°κ²©(μ¬λ°±) μ΅μ ν κ°μ (2026-05-20)**:
- **νμ**: λ©μΈ νλ©΄ μ€λ₯Έμͺ½ 컬λΌμμ μ±λ΄ μΈν°νμ΄μ€μ κ°λ³ μ»΄ν¬λνΈ(μκ° λ³΄λ, μμ μ§λ¬Έ λ²νΌ, λ©μμ§ λ²λΈ, μ
λ ₯μ°½)κ° νλ©΄μ 100% κ½ μ±μ λ€μ μκ°μ μΌλ‘ νΌμ Έ 보μ΄κ³ κ°λ
μ±μ΄ μ νλλ λ¬Έμ λ°μ. λν μλ¨ GNBμ μ±λ΄ μ¬μ΄μ μμ§ μ¬λ°± λ° μ±λ΄ λ΄ μ»΄ν¬λνΈ κ° κ°κ²©μ΄ λ무 컀μ κ³΅κ° λλΉ λ°μ.
- **μ‘°μΉ**:
1. **μ°μΈ‘ Column ID μ§μ **: `app.py`μμ μ°μΈ‘ μ±λ΄ μ»΄ν¬λνΈλ₯Ό λ΄λ Columnμ `elem_id="chat-column"`μ κ³ μ νκ² μ§μ .
2. **컨ν
μ΄λ κΈ°λ° 70% λλΉ ν΅μ **: `src/utils/ui_templates.py`μ `CUSTOM_CSS`μμ `#chat-column > div`λ₯Ό μ§μ νμ¬ μ±λ΄ μ΅μΈκ³½ νλ μ μ 체λ₯Ό `70%` λλΉλ‘ μ ννκ³ `margin: 0 auto`λ‘ μ€μ μ λ ¬μ κ°μ . μ΄μ λ§μΆ° λ΄λΆ μμ μμλ€(`.placeholder`, `.examples-container`, `.message-wrap`, `.input-container`)μ `width: 100%`λ‘ λΆλͺ¨ 컨ν
μ΄λμ λ± λ€μ΄λ§κ² μ λ ¬νμ¬ λ μ΄μμ μ΄κΈλ¨ μμ² μ κ±°.
3. **μμ§ μ¬λ°± λν κΈ΄λ°ν**: GNB μλμ λ°ν
λ§μ§μ `20px`μμ `6px`λ‘ μ€μ΄κ³ ν¨λ©μ μμΆ. μ±λ΄ λ΄λΆμ μ»΄ν¬λνΈ κ° κ°κ²©(`gap` λ° `margin`)κ³Ό κ°λ³ 보λμ μμͺ½ ν¨λ©(`padding`)μ μ 체μ μΌλ‘ μ€μ¬(μ: μκ°κΈ ν¨λ© `10px 14px`, λ§μ§ `4px auto 6px auto` λ±) νλ©΄ λ΄μ νλμ μ λ€μ΄μ€λλ‘ μ΅μ ν.
4. **λ°μν λͺ¨λ°μΌ λ―Έλμ΄ μΏΌλ¦¬ κ°±μ **: κ°λ‘ 800px μ΄ν λͺ¨λ°μΌ νλ©΄μμλ μλμΌλ‘ 100% κ½ μ°¨λλ‘ κ°±μ νμ¬ ν리미μ UXλ₯Ό μλ²½νκ² μ μ§.
- **κ²μ¦**: `ruff`μ `mypy` κ²μ¬λ₯Ό 무μ€λ₯ ν΅κ³Όν¨. `python -c "import app"`μΌλ‘ Gradio μΉμ± λΉλ 무결μ±μ μ΅μ’
ν보ν¨.
- [x] **GraphRAG κ²μ κΈ°μ¬ λλ―Έ URL μμ² κ΅μ²΄ λ° DB λ°μ (2026-05-21)**:
- **νμ**: GraphRAG κ²μ κ²°κ³Όμμ μ 곡λλ 'κ·Όκ±° λ΄μ€ μΆμ²' λ§ν¬(URL)κ° μ κ·Όν μ μλ κ°μ§ λ€μ΄λ² λ΄μ€ URL νμ(`news.naver.com/main/read.naver?...&oid=001&aid=11111111` λ±)μΌλ‘ νλμ½λ© λμ΄ μμ΄ μΆμ² νμΈ λΆκ°λ₯.
- **μ‘°μΉ**:
1. **μ ν/μΉ΄μΉ΄μ€/ν μ€/λ€μ΄λ²νμ΄ 4λ ν΅μ¬ μ£Όμ **μ μ€μ‘΄νλ 곡μ λ ₯ μλ μΈλ‘ μ¬ κΈ°μ¬(νκ΅κ²½μ λ§€κ±°μ§, λ΄μμ€, λμ§νΈνμμ€, λλ°Έλ₯λ΄μ€)μ μ€μ URL μ£Όμλ₯Ό κ²μ¦.
2. `inject_fintech_gold_data.py` νμΌ λ΄ νλμ½λ©λ λλ―Έ URL 4건μ μ€μ URLλ‘ κ΅μ²΄.
3. Neo4j AuraDBμ μ μνμ¬ κΈ°μ‘΄ μ μ¬λ `Article` λ
Έλλ€μ λλ―Έ URLμ μ€μ URLλ‘ μ§μ `MERGE` μ
λ°μ΄νΈνλ μμ μ€ν¬λ¦½νΈλ₯Ό μμ±νμ¬ DB μ
λ°μ΄νΈ μλ£.
4. μ½λ 무결μ±(ruff, mypy) λ° λ³΄μ 무결μ±(bandit High/Medium μ·¨μ½μ 0건) κ²μ¦ ν μ격 λ°°ν¬(Git push).
- **κ²μ¦**: Git pre-push hookμμ `pytest` λ° `smoke_test_rag.py` 100% PASS νμΈ μλ£.
|