Spaces:
Sleeping
Sleeping
ใ
ใ
ใ
commited on
Commit
ยท
9803acf
1
Parent(s):
d4a4cca
Update app.py: Change logging level from INFO to WARNING
Browse files- CodeWeaver +0 -1
- CodeWeaver/.env.example +9 -0
- CodeWeaver/.gitignore +23 -0
- CodeWeaver/.python-version +1 -0
- CodeWeaver/IMPLEMENTATION_REPORT.md +175 -0
- CodeWeaver/PHASE3_CHANGES.md +142 -0
- CodeWeaver/PHASE5_SUBGRAPH_REFACTORING.md +320 -0
- CodeWeaver/README.md +118 -0
- CodeWeaver/main.py +6 -0
- CodeWeaver/pyproject.toml +27 -0
- CodeWeaver/requirements.txt +24 -0
- CodeWeaver/src/__init__.py +0 -0
- CodeWeaver/src/agent/__init__.py +51 -0
- CodeWeaver/src/agent/graph.py +420 -0
- CodeWeaver/src/agent/nodes.py +1212 -0
- CodeWeaver/src/agent/state.py +141 -0
- CodeWeaver/src/tools/__init__.py +12 -0
- CodeWeaver/src/tools/search_tools.py +217 -0
- CodeWeaver/src/utils/__init__.py +7 -0
- CodeWeaver/src/utils/tracing.py +91 -0
- CodeWeaver/src/vector_db/__init__.py +6 -0
- CodeWeaver/src/vector_db/local_embeddings.py +34 -0
- CodeWeaver/src/vector_db/qdrant_client.py +225 -0
- CodeWeaver/test_result.txt +56 -0
- CodeWeaver/ui/app.py +272 -0
CodeWeaver
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit fc4c811e94059981ae4ef7924c9aed6ccc9cbc44
|
|
|
|
|
|
CodeWeaver/.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GOOGLE_API_KEY=your-google-api-key
|
| 2 |
+
TAVILY_API_KEY=your-tavily-api-key
|
| 3 |
+
QDRANT_URL=https://your-qdrant-endpoint
|
| 4 |
+
QDRANT_API_KEY=your-qdr
|
| 5 |
+
LANGCHAIN_TRACING_V2=true
|
| 6 |
+
LANGCHAIN_API_KEY=your_langsmith_api_key_here
|
| 7 |
+
LANGCHAIN_PROJECT=codeweaver
|
| 8 |
+
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
|
| 9 |
+
GITHUB_TOKEN=
|
CodeWeaver/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# Environment variables (๋ฏผ๊ฐํ ์ ๋ณด ํฌํจ)
|
| 13 |
+
.env
|
| 14 |
+
|
| 15 |
+
# IDE
|
| 16 |
+
.vscode/
|
| 17 |
+
.idea/
|
| 18 |
+
*.swp
|
| 19 |
+
*.swo
|
| 20 |
+
|
| 21 |
+
# OS
|
| 22 |
+
.DS_Store
|
| 23 |
+
Thumbs.db
|
CodeWeaver/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
CodeWeaver/IMPLEMENTATION_REPORT.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CodeWeaver Phase 3 ๊ตฌํ ์๋ฃ ๋ณด๊ณ ์
|
| 2 |
+
|
| 3 |
+
## ์๋ฃ ๋ ์ง
|
| 4 |
+
2024-12-20
|
| 5 |
+
|
| 6 |
+
## ๊ตฌํ ๋ชฉํ
|
| 7 |
+
Open Deep Research ํจํด์ ์ ์ฉํ์ฌ ๊ฒ์ ํ์ง๊ณผ ๋ต๋ณ ์ ํ๋๋ฅผ ํฅ์
|
| 8 |
+
|
| 9 |
+
## ๊ตฌํ๋ ๊ธฐ๋ฅ
|
| 10 |
+
|
| 11 |
+
### 1. ํญ์ ์ง๋ฌธ ๋ถํด (create_plan_node)
|
| 12 |
+
- **์์น**: `src/agent/nodes.py:203-287`
|
| 13 |
+
- **๋์**: ๋ชจ๋ ์ง๋ฌธ์ 1-5๊ฐ์ ์๋ธ ์ง๋ฌธ์ผ๋ก ๋ถํด
|
| 14 |
+
- **์ ๋ต**:
|
| 15 |
+
- ๋จ์ ์ง๋ฌธ โ 1๊ฐ ์๋ธ ์ง๋ฌธ
|
| 16 |
+
- ๋ณต์ก ์ง๋ฌธ โ 3-5๊ฐ ์๋ธ ์ง๋ฌธ
|
| 17 |
+
- **LLM ์ฌ์ฉ**: JSON ๊ตฌ์กฐํ๋ ์ถ๋ ฅ
|
| 18 |
+
|
| 19 |
+
### 2. ๊ฒ์ ๊ฒฐ๊ณผ ์์ง (collect_results_node)
|
| 20 |
+
- **์์น**: `src/agent/nodes.py:461-479`
|
| 21 |
+
- **์ญํ **: Fan-in ํฌ์ธํธ, 3๊ฐ ๋ณ๋ ฌ ๊ฒ์ ๋
ธ๋์ ๊ฒฐ๊ณผ ์ง๊ณ
|
| 22 |
+
- **์ถ๋ ฅ**: `len(search_results)` ๊ธฐ์ค์ผ๋ก ์์ ๊ฒฐ๊ณผ ์ ํ๊ฐ (ํ๋ ์ ์ฅ ์ ๊ฑฐ)
|
| 23 |
+
|
| 24 |
+
### 3. ๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ (evaluate_results_node)
|
| 25 |
+
- **์์น**: `src/agent/nodes.py:482-533`
|
| 26 |
+
- **์๊ณ๊ฐ**: 2๊ฐ ๋ฏธ๋ง์ด๋ฉด ๊ฐ์ ํ์
|
| 27 |
+
- **์์ ์ฅ์น**: refinement_count >= 1์ด๋ฉด ๋ฌด์กฐ๊ฑด ์งํ
|
| 28 |
+
- **์ถ๋ ฅ**: `needs_refinement` (boolean)
|
| 29 |
+
|
| 30 |
+
### 4. ์ค๋งํธ ์ฟผ๋ฆฌ ๊ฐ์ (refine_search_node)
|
| 31 |
+
- **์์น**: `src/agent/nodes.py:536-633`
|
| 32 |
+
- **์ ๋ต ์ ํ** (LLM):
|
| 33 |
+
- MORE_SPECIFIC: ๊ธฐ์ ์ ์ธ๋ถ์ฌํญ ์ถ๊ฐ
|
| 34 |
+
- MORE_GENERAL: ๋ ๋์ ์ฉ์ด ์ฌ์ฉ
|
| 35 |
+
- TRANSLATE: ์ธ์ด ๋ณํ
|
| 36 |
+
- **์๋ณธ ๋ณด์กด**: `original_question` ํ๋์ ์ ์ฅ
|
| 37 |
+
|
| 38 |
+
### 5. ๊ทธ๋ํ ์ฌ๊ตฌ์ฑ
|
| 39 |
+
- **์์น**: `src/agent/graph.py:200-330`
|
| 40 |
+
- **์๋ก์ด ์ฃ์ง**:
|
| 41 |
+
- `check_cache` โ `create_plan` (์บ์ ๋ฏธ์ค ์)
|
| 42 |
+
- `create_plan` โ `classify_intent`
|
| 43 |
+
- `search_*` โ `collect_results` (fan-in)
|
| 44 |
+
- `collect_results` โ `evaluate_results`
|
| 45 |
+
- `evaluate_results` โ `refine_search` or `search_subgraph`
|
| 46 |
+
- `refine_search` โ `classify_intent` (๋ฃจํ)
|
| 47 |
+
|
| 48 |
+
### 6. ์ํ ์คํค๋ง ํ์ฅ
|
| 49 |
+
- **์์น**: `src/agent/state.py:127-143`
|
| 50 |
+
- **์ถ๊ฐ ํ๋**:
|
| 51 |
+
```python
|
| 52 |
+
plan: Optional[Dict[str, Any]]
|
| 53 |
+
needs_refinement: bool
|
| 54 |
+
refinement_count: int
|
| 55 |
+
original_question: Optional[str]
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## ํ
์คํธ ๊ฒฐ๊ณผ
|
| 59 |
+
|
| 60 |
+
### ํตํฉ ํ
์คํธ (test_new_features.py)
|
| 61 |
+
- โ
ํ
์คํธ 1: ๋จ์ ์ง๋ฌธ - ์ ์ ๋์
|
| 62 |
+
- โ
ํ
์คํธ 2: ๋ณต์ก ์ง๋ฌธ - ์ ์ ๋์
|
| 63 |
+
- โ
ํ
์คํธ 3: ๊ฒฐ๊ณผ ๋ถ์กฑ ์๋๋ฆฌ์ค - ์ฟผ๋ฆฌ ๊ฐ์ ํ์ธ
|
| 64 |
+
- โ
ํ
์คํธ 4: ๊ฐ์ ์ ํ - ์ต๋ 1ํ ๋ณด์ฅ
|
| 65 |
+
|
| 66 |
+
### ์คํ ํต๊ณ
|
| 67 |
+
```
|
| 68 |
+
[PASS] Passed: 4/4
|
| 69 |
+
[FAIL] Failed: 0/4
|
| 70 |
+
[SUCCESS] All tests passed!
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### ์ค์ ๋์ ๊ฒ์ฆ
|
| 74 |
+
```
|
| 75 |
+
INFO:src.agent.nodes:์ง๋ฌธ ๋ถํด ๊ณํ ์๋ฆฝ ์ค
|
| 76 |
+
INFO:src.agent.nodes:๊ณํ ์๋ฆฝ ์๋ฃ: 4๊ฐ ์๋ธ ์ง๋ฌธ
|
| 77 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: 0๊ฐ
|
| 78 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: 0๊ฐ (๊ฐ์ ํ์: 0)
|
| 79 |
+
INFO:src.agent.nodes:์ฟผ๋ฆฌ ๊ฐ์ ์ค
|
| 80 |
+
INFO:src.agent.nodes:์ฟผ๋ฆฌ ๊ฐ์ ์๋ฃ
|
| 81 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: 11๊ฐ
|
| 82 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: 11๊ฐ (๊ฐ์ ํ์: 1)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## ์ค์ํ LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ
|
| 86 |
+
|
| 87 |
+
### 1. ๋
ธ๋๋ ํ ๊ฐ์ง ์ผ๋ง ์ํ โ
|
| 88 |
+
- ๊ฐ ๋
ธ๋๊ฐ ๋จ์ผ ์ฑ
์ ์์น ์ค์
|
| 89 |
+
- `create_plan`: ์ง๋ฌธ ๋ถํด๋ง
|
| 90 |
+
- `evaluate_results`: ํ๊ฐ๋ง (๋ผ์ฐํ
X)
|
| 91 |
+
|
| 92 |
+
### 2. ์ํ์ ์์ ๋ฐ์ดํฐ ์ ์ฅ โ
|
| 93 |
+
- ํฌ๋งท๋ ํ
์คํธ X
|
| 94 |
+
- ๊ณ์ฐ ๊ฐ๋ฅํ ๊ฐ X
|
| 95 |
+
- ์์ ๋ฐ์ดํฐ๋ง ์ ์ฅ
|
| 96 |
+
|
| 97 |
+
### 3. ํ๋กฌํํธ๋ ๋
ธ๋ ๋ด์์ ์์ฑ โ
|
| 98 |
+
- ์ํ์ ํ๋กฌํํธ ํ
ํ๋ฆฟ ์ ์ฅ X
|
| 99 |
+
- ๊ฐ ๋
ธ๋์์ ๋์ ์์ฑ
|
| 100 |
+
|
| 101 |
+
### 4. Send API๋ก ๋ณ๋ ฌ ์คํ โ
|
| 102 |
+
- 3๊ฐ ๊ฒ์ ๋
ธ๋ ๋์ ์คํ
|
| 103 |
+
- reducer๋ก ์๋ ๋จธ์ง
|
| 104 |
+
|
| 105 |
+
### 5. ์ฒดํฌํฌ์ธํ
์ง์ โ
|
| 106 |
+
- ๋ชจ๋ ๋
ธ๋ ๊ฒฝ๊ณ์์ ์ํ ์ ์ฅ
|
| 107 |
+
- ์ธ์ ๋ ์ฌ๊ฐ ๊ฐ๋ฅ
|
| 108 |
+
|
| 109 |
+
## ์ฑ๋ฅ ๊ฐ์ ์งํ
|
| 110 |
+
|
| 111 |
+
### ๊ฒ์ ํ์ง
|
| 112 |
+
- Before: ๋จ์ผ ๊ฒ์ โ ๊ฒฐ๊ณผ 0๊ฐ ์ ์คํจ
|
| 113 |
+
- After: ์๋ ๊ฐ์ โ ์ฌ๊ฒ์ โ ์ฑ๊ณต๋ฅ โ
|
| 114 |
+
|
| 115 |
+
### ๋ต๋ณ ์ ํ๋
|
| 116 |
+
- Before: ๋ชจํธํ ๊ฒ์์ด โ ๋ถ์ ์ ํ ๊ฒฐ๊ณผ
|
| 117 |
+
- After: ์ง๋ฌธ ๋ถํด + ์ฟผ๋ฆฌ ๊ฐ์ โ ์ ํ๋ โ
|
| 118 |
+
|
| 119 |
+
### ์์ ์ฑ
|
| 120 |
+
- Before: ๋ฌดํ ๋ฃจํ ๊ฐ๋ฅ์ฑ
|
| 121 |
+
- After: refinement_count ์ ํ์ผ๋ก ๋ณด์ฅ
|
| 122 |
+
|
| 123 |
+
## ํ์ผ ๋ณ๊ฒฝ ์์ฝ
|
| 124 |
+
|
| 125 |
+
### ์์ ๋ ํ์ผ (3๊ฐ)
|
| 126 |
+
1. `src/agent/state.py` - 5๊ฐ ํ๋ ์ถ๊ฐ
|
| 127 |
+
2. `src/agent/nodes.py` - 4๊ฐ ๋
ธ๋ ์ถ๊ฐ/์์
|
| 128 |
+
3. `src/agent/graph.py` - ์ฃ์ง ์ฌ๊ตฌ์ฑ, 2๊ฐ ๋ผ์ฐํ
ํจ์ ์ถ๊ฐ
|
| 129 |
+
|
| 130 |
+
### ์ถ๊ฐ๋ ํ์ผ (3๊ฐ)
|
| 131 |
+
1. `test_new_features.py` - ํตํฉ ํ
์คํธ
|
| 132 |
+
2. `PHASE3_CHANGES.md` - ๋ณ๊ฒฝ์ฌํญ ๋ฌธ์
|
| 133 |
+
3. `demo_phase3.py` - ๋ฐ๋ชจ ์คํฌ๋ฆฝํธ
|
| 134 |
+
|
| 135 |
+
### ์์ ๋ ๋ฌธ์ (1๊ฐ)
|
| 136 |
+
1. `README.md` - Phase 3 ์น์
์ถ๊ฐ
|
| 137 |
+
|
| 138 |
+
## ์ฝ๋ ํต๊ณ
|
| 139 |
+
- ์ถ๊ฐ๋ ๋ผ์ธ: ~500์ค
|
| 140 |
+
- ์์ ๋ ๋ผ์ธ: ~50์ค
|
| 141 |
+
- ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง: 4๊ฐ ์๋๋ฆฌ์ค
|
| 142 |
+
|
| 143 |
+
## ๋ค์ ๋จ๊ณ ์ ์
|
| 144 |
+
|
| 145 |
+
### ๋จ๊ธฐ (1-2์ฃผ)
|
| 146 |
+
1. ์๋ธ ์ง๋ฌธ๋ณ ๋ณ๋ ฌ ๊ฒ์ ๊ตฌํ
|
| 147 |
+
2. ์ ์ํ ์๊ณ๊ฐ (์ง๋ฌธ ๋ณต์ก๋ ๊ธฐ๋ฐ)
|
| 148 |
+
3. UI์ ๊ณํ ์๋ฆฝ ๋จ๊ณ ํ์
|
| 149 |
+
|
| 150 |
+
### ์ค๊ธฐ (1-2๊ฐ์)
|
| 151 |
+
1. ๊ฐ์ ์ ๋ต ํ์ต ์์คํ
|
| 152 |
+
2. ๋ค๋จ๊ณ ๊ฐ์ (์ต๋ 2-3ํ)
|
| 153 |
+
3. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋
|
| 154 |
+
|
| 155 |
+
### ์ฅ๊ธฐ (3-6๊ฐ์)
|
| 156 |
+
1. ๋ค๊ตญ์ด ์ง์ ๊ฐํ
|
| 157 |
+
2. ๋๋ฉ์ธ๋ณ ์ ๋ฌธํ
|
| 158 |
+
3. ์ฌ์ฉ์ ํผ๋๋ฐฑ ๊ธฐ๋ฐ ๊ฐ์
|
| 159 |
+
|
| 160 |
+
## ์๋ ค์ง ์ ํ์ฌํญ
|
| 161 |
+
|
| 162 |
+
1. **์บ์ ์ฐ์ ์์**: ์บ์ ํํธ ์ ๊ณํ ์๋ฆฝ ๊ฑด๏ฟฝ๏ฟฝ๏ฟฝ๋ (์๋๋ ๋์)
|
| 163 |
+
2. **Windows ์ฝ์**: ์ด๋ชจ์ง ์ธ์ฝ๋ฉ ์ด์ (๋ก์ง์ ์ ์)
|
| 164 |
+
3. **GitHub API**: ์ผ๋ถ ์ฟผ๋ฆฌ์์ 422 ์๋ฌ (์ธ๋ถ API ์ ์ฝ)
|
| 165 |
+
|
| 166 |
+
## ๊ฒฐ๋ก
|
| 167 |
+
|
| 168 |
+
โ
Open Deep Research ํจํด ์ฑ๊ณต์ ์ผ๋ก ์ ์ฉ
|
| 169 |
+
โ
๋ชจ๋ ํ
์คํธ ํต๊ณผ
|
| 170 |
+
โ
LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ ์ค์
|
| 171 |
+
โ
๊ธฐ์กด ๊ธฐ๋ฅ ์๋ฒฝ ํธํ
|
| 172 |
+
|
| 173 |
+
Phase 3 ๊ตฌํ์ด ์๋ฃ๋์์ผ๋ฉฐ, ํ๋ก๋์
๋ฐฐํฌ ์ค๋น๊ฐ ์๋ฃ๋์์ต๋๋ค.
|
| 174 |
+
|
| 175 |
+
|
CodeWeaver/PHASE3_CHANGES.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 3: Open Deep Research ํจํด ์ ์ฉ
|
| 2 |
+
|
| 3 |
+
## ๊ฐ์
|
| 4 |
+
|
| 5 |
+
CodeWeaver์ [Open Deep Research](https://github.com/langchain-ai/open_deep_research) ํจํด์ ์ ์ฉํ์ฌ ๊ฒ์ ํ์ง๊ณผ ๋ต๋ณ ์ ํ๋๋ฅผ ํฅ์์์ผฐ์ต๋๋ค.
|
| 6 |
+
|
| 7 |
+
## ๋ณ๊ฒฝ๋ ํ์ผ
|
| 8 |
+
|
| 9 |
+
### 1. `src/agent/state.py`
|
| 10 |
+
**์ถ๊ฐ๋ ํ๋:**
|
| 11 |
+
```python
|
| 12 |
+
# Planning & Refinement (Phase 3)
|
| 13 |
+
plan: Optional[Dict[str, Any]] # ์ง๋ฌธ ๋ถํด ๊ณํ
|
| 14 |
+
needs_refinement: bool # ์ฟผ๋ฆฌ ๊ฐ์ ํ์ ์ฌ๋ถ
|
| 15 |
+
needs_refinement: bool # ์ฟผ๋ฆฌ ๊ฐ์ ํ์ ์ฌ๋ถ
|
| 16 |
+
refinement_count: int # ๊ฐ์ ์๋ ํ์ (์ต๋ 1ํ)
|
| 17 |
+
original_question: Optional[str] # ์๋ณธ ์ง๋ฌธ ๋ณด์กด
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
### 2. `src/agent/nodes.py`
|
| 21 |
+
**์ถ๊ฐ๋ ๋
ธ๋ (4๊ฐ):**
|
| 22 |
+
- `create_plan_node`: ๋ชจ๋ ์ง๋ฌธ์ ์๋ธ ์ง๋ฌธ์ผ๋ก ๋ถํด
|
| 23 |
+
- `collect_results_node`: ๋ณ๋ ฌ ๊ฒ์ ๊ฒฐ๊ณผ ์์ง (fan-in)
|
| 24 |
+
- `evaluate_results_node`: ๊ฒฐ๊ณผ ์ ํ๊ฐ (< 2๊ฐ๋ฉด ๊ฐ์ ํ์)
|
| 25 |
+
- `refine_search_node`: LLM ๊ธฐ๋ฐ ์ฟผ๋ฆฌ ๊ฐ์ (์ ๋ต ์ ํ)
|
| 26 |
+
|
| 27 |
+
### 3. `src/agent/graph.py`
|
| 28 |
+
**์์ ๋ ๋ผ์ฐํ
:**
|
| 29 |
+
- `route_after_cache`: ์บ์ ๋ฏธ์ค ์ โ `create_plan` (๊ธฐ์กด: `classify_intent`)
|
| 30 |
+
- `route_after_evaluation`: ์๋ก์ด ๋ผ์ฐํ
ํจ์ ์ถ๊ฐ
|
| 31 |
+
- ๊ฒฐ๊ณผ ๋ถ์กฑ & refinement_count=0 โ `refine_search`
|
| 32 |
+
- ๊ฒฐ๊ณผ ์ถฉ๋ถ or refinement_count=1 โ `search_subgraph`
|
| 33 |
+
|
| 34 |
+
**์ถ๊ฐ๋ ์ฃ์ง:**
|
| 35 |
+
- `create_plan` โ `classify_intent`
|
| 36 |
+
- `search_*` โ `collect_results` (fan-in)
|
| 37 |
+
- `collect_results` โ `evaluate_results`
|
| 38 |
+
- `evaluate_results` โฒ `refine_search` โ `classify_intent` (๋ฃจํ)
|
| 39 |
+
|
| 40 |
+
## ์๋ก์ด ์ํฌํ๋ก์ฐ
|
| 41 |
+
|
| 42 |
+
### Before (Phase 2)
|
| 43 |
+
```
|
| 44 |
+
check_cache โ classify_intent โ parallel_search โ search_subgraph โ generate_answer
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### After (Phase 3)
|
| 48 |
+
```
|
| 49 |
+
check_cache โ create_plan โ classify_intent โ parallel_search
|
| 50 |
+
โ collect_results โ evaluate_results
|
| 51 |
+
โโ < 2 results โ refine_search โฒ classify_intent (์ต๋ 1ํ)
|
| 52 |
+
โโ >= 2 results โ search_subgraph โ generate_answer
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## ํต์ฌ ์ค๊ณ ์์น (LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ)
|
| 56 |
+
|
| 57 |
+
### 1. ๋
ธ๋๋ ํ ๊ฐ์ง ์ผ๋ง ์ํ
|
| 58 |
+
โ
`create_plan`: ์ง๋ฌธ ๋ถํด๋ง
|
| 59 |
+
โ
`collect_results`: ๊ฒฐ๊ณผ ์์ง๋ง
|
| 60 |
+
โ
`evaluate_results`: ํ๊ฐ๋ง (๋ผ์ฐํ
X)
|
| 61 |
+
โ
`refine_search`: ์ฟผ๋ฆฌ ๊ฐ์ ๋ง
|
| 62 |
+
|
| 63 |
+
### 2. ๋ผ์ฐํ
์ conditional_edges์์
|
| 64 |
+
```python
|
| 65 |
+
graph.add_conditional_edges(
|
| 66 |
+
"evaluate_results",
|
| 67 |
+
route_after_evaluation, # ๋ผ์ฐํ
ํจ์
|
| 68 |
+
{
|
| 69 |
+
"refine_search": "refine_search",
|
| 70 |
+
"search_subgraph": "search_subgraph"
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### 3. ์ํ์๋ ์์ ๋ฐ์ดํฐ๋ง ์ ์ฅ
|
| 76 |
+
```python
|
| 77 |
+
# โ
Good: ์์ ๋ฐ์ดํฐ
|
| 78 |
+
search_results: list[SearchResult]
|
| 79 |
+
needs_refinement: bool
|
| 80 |
+
|
| 81 |
+
# โ Bad: ๊ณ์ฐ๋ ๊ฐ์ด๋ ํฌ๋งท๋ ํ
์คํธ
|
| 82 |
+
formatted_prompt: str
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### 4. ํ๋กฌํํธ๋ ๋
ธ๋ ๋ด์์ ๋์ ์์ฑ
|
| 86 |
+
```python
|
| 87 |
+
def refine_search_node(state: AgentState) -> dict:
|
| 88 |
+
# โ
๋
ธ๋ ๋ด์์ ๋์ ์ผ๋ก ํ๋กฌํํธ ๊ตฌ์ฑ
|
| 89 |
+
refinement_prompt = f"""
|
| 90 |
+
Original question: {state.user_question}
|
| 91 |
+
Current results: {len(state.search_results)}
|
| 92 |
+
...
|
| 93 |
+
"""
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## ํ
์คํธ ๊ฒฐ๊ณผ
|
| 97 |
+
|
| 98 |
+
### ํต๊ณผํ ์๋๋ฆฌ์ค
|
| 99 |
+
1. โ
๋จ์ ์ง๋ฌธ: 1๊ฐ ์๋ธ ์ง๋ฌธ ์์ฑ โ ์ ์ ์งํ
|
| 100 |
+
2. โ
๋ณต์ก ์ง๋ฌธ: 3-5๊ฐ ์๋ธ ์ง๋ฌธ ์์ฑ โ ์ ์ ์งํ
|
| 101 |
+
3. โ
๊ฒฐ๊ณผ ๋ถ์กฑ: < 2๊ฐ ๊ฒฐ๊ณผ โ ์ฟผ๋ฆฌ ๊ฐ์ โ ์ฌ๊ฒ์
|
| 102 |
+
4. โ
๊ฐ์ ์ ํ: refinement_count ์ต๋ 1ํ ๋ณด์ฅ
|
| 103 |
+
|
| 104 |
+
### ์คํ ๋ก๊ทธ ์์
|
| 105 |
+
```
|
| 106 |
+
INFO:src.agent.nodes:์ง๋ฌธ ๋ถํด ๊ณํ ์๋ฆฝ ์ค: What is GraphQL endpoint design pattern?
|
| 107 |
+
INFO:src.agent.nodes:๊ณํ ์๋ฆฝ ์๋ฃ: 4๊ฐ ์๋ธ ์ง๋ฌธ
|
| 108 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: 0๊ฐ
|
| 109 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: 0๊ฐ (๊ฐ์ ํ์: 0)
|
| 110 |
+
INFO:src.agent.nodes:์ฟผ๋ฆฌ ๊ฐ์ ์ค: What is GraphQL endpoint design pattern? (0๊ฐ ๊ฒฐ๊ณผ)
|
| 111 |
+
INFO:src.agent.nodes:์ฟผ๋ฆฌ ๊ฐ์ ์๋ฃ: GraphQL API design best practices
|
| 112 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: 11๊ฐ
|
| 113 |
+
INFO:src.agent.nodes:๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: 11๊ฐ (๊ฐ์ ํ์: 1)
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## ์ฑ๋ฅ ๊ฐ์
|
| 117 |
+
|
| 118 |
+
### ๊ฒ์ ํ์ง
|
| 119 |
+
- **Before**: ๋จ์ผ ๊ฒ์ โ ๊ฒฐ๊ณผ ๋ถ์กฑ ์ ์คํจ
|
| 120 |
+
- **After**: ๊ฒฐ๊ณผ ๋ถ์กฑ ์ ์๋ ๊ฐ์ โ ์ฌ๊ฒ์
|
| 121 |
+
|
| 122 |
+
### ๋ต๋ณ ์ ํ๋
|
| 123 |
+
- **Before**: ๋ชจํธํ ์ง๋ฌธ โ ๋ถ์ ํํ ๊ฒ์
|
| 124 |
+
- **After**: ์๋ธ ์ง๋ฌธ ๋ถํด โ ๋ ๊ตฌ์ฒด์ ์ธ ๊ฒ์
|
| 125 |
+
|
| 126 |
+
### ์์ ์ฑ
|
| 127 |
+
- **Before**: ๋ฌดํ ๋ฃจํ ๊ฐ๋ฅ์ฑ
|
| 128 |
+
- **After**: refinement_count ์ ํ์ผ๋ก ๋ณด์ฅ
|
| 129 |
+
|
| 130 |
+
## ํฅํ ๊ฐ์ ๋ฐฉํฅ
|
| 131 |
+
|
| 132 |
+
1. **์๋ธ ์ง๋ฌธ ๋ณ๋ ฌ ๊ฒ์**: ํ์ฌ๋ ์ ์ฒด ์ง๋ฌธ์ผ๋ก ๊ฒ์, ๊ฐ ์๋ธ ์ง๋ฌธ๋ณ ๊ฒ์์ผ๋ก ํ์ฅ
|
| 133 |
+
2. **์ ์ํ ์๊ณ๊ฐ**: ํ์ฌ ๊ณ ์ ๊ฐ 2๊ฐ โ ์ง๋ฌธ ๋ณต์ก๋์ ๋ฐ๋ผ ๋์ ์กฐ์
|
| 134 |
+
3. **๊ฐ์ ์ ๋ต ํ์ต**: LLM ์ ํ โ ๊ณผ๊ฑฐ ์ฑ๊ณต ์ ๋ต ๊ธฐ๋ฐ ์ถ์ฒ
|
| 135 |
+
4. **๋ค๋จ๊ณ ๊ฐ์ **: ์ต๋ 1ํ โ 2-3ํ๋ก ํ์ฅ (์ํ ๊ฐ์ง ์ถ๊ฐ)
|
| 136 |
+
|
| 137 |
+
## ์ฐธ๊ณ ์๋ฃ
|
| 138 |
+
|
| 139 |
+
- [LangGraph Official Guide: Thinking in LangGraph](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph)
|
| 140 |
+
- [Open Deep Research GitHub](https://github.com/langchain-ai/open_deep_research)
|
| 141 |
+
|
| 142 |
+
|
CodeWeaver/PHASE5_SUBGRAPH_REFACTORING.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 5: ์๋ธ๊ทธ๋ํ ๋ฆฌํฉํ ๋ง ์๋ฃ ๋ณด๊ณ ์
|
| 2 |
+
|
| 3 |
+
## ๊ฐ์
|
| 4 |
+
|
| 5 |
+
๋ณต์กํ๊ฒ ์ฝํ ๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ ๋ก์ง์ ๋จ์ํํ๊ธฐ ์ํด, **analyze_question๋ถํฐ generate_answer๊น์ง๋ฅผ ๋
๋ฆฝ๋ ์๋ธ๊ทธ๋ํ๋ก ์ถ์ถ**ํ๊ณ , ๋ถ๋ชจ ๊ทธ๋ํ๋ ๊ณํ/๋ถ๊ธฐ/๋ณํฉ๋ง ๋ด๋นํ๋๋ก ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ํ์ต๋๋ค.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## ๋ชฉํ ๋ฌ์ฑ ์ฌ๋ถ
|
| 10 |
+
|
| 11 |
+
โ
**๋ชจ๋ ๋ชฉํ ๋ฌ์ฑ ์๋ฃ**
|
| 12 |
+
|
| 13 |
+
1. โ
๋จ์ผ ์ง๋ฌธ ํ์ดํ๋ผ์ธ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์๋ธ๊ทธ๋ํ๋ก ์ถ์ถ
|
| 14 |
+
2. โ
๋ถ๋ชจ ๊ทธ๋ํ ๋จ์ํ (orchestration๋ง ๋ด๋น)
|
| 15 |
+
3. โ
๋ณต์กํ worker ๋
ธ๋ ๋ฐ ์ค๋ณต ๊ทธ๋ํ ๋น๋ ์ ๊ฑฐ
|
| 16 |
+
4. โ
๊ตฌ์กฐ ๋ช
ํํ: ๋ถ๋ชจ(orchestration) vs ์์(processing)
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## ๋ณ๊ฒฝ ์ฌํญ
|
| 21 |
+
|
| 22 |
+
### 1. ์๋ก์ด ์๋ธ๊ทธ๋ํ: `build_single_question_subgraph()`
|
| 23 |
+
|
| 24 |
+
**ํ์ผ**: [`src/agent/graph.py`](src/agent/graph.py)
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
def build_single_question_subgraph() -> StateGraph:
|
| 28 |
+
"""
|
| 29 |
+
๋จ์ผ ์ง๋ฌธ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ ์๋ธ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
|
| 30 |
+
|
| 31 |
+
์ง์
์ : analyze_question (START โ analyze_question)
|
| 32 |
+
์ถ๊ตฌ: generate_answer ๋๋ generate_with_history ๋๋ return_cached_answer (โ END)
|
| 33 |
+
|
| 34 |
+
ํ๋ฆ:
|
| 35 |
+
1. analyze_question โ ์ง๋ฌธ ๋ถ์
|
| 36 |
+
- clarification: generate_with_history โ END
|
| 37 |
+
- new_topic/independent: check_cache
|
| 38 |
+
2. check_cache โ ์บ์ ํ์ธ
|
| 39 |
+
- ํํธ: return_cached_answer โ END
|
| 40 |
+
- ๋ฏธ์ค: classify_intent
|
| 41 |
+
3. classify_intent โ ๋ณ๋ ฌ ๊ฒ์ (Send API)
|
| 42 |
+
4. ๊ฒ์ ๊ฒฐ๊ณผ ์์ง โ ํ๊ฐ โ ํํฐ๋ง โ ์์ฝ โ ๋ต๋ณ ์์ฑ
|
| 43 |
+
"""
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
**ํฌํจ ๋
ธ๋**:
|
| 47 |
+
- analyze_question, generate_with_history
|
| 48 |
+
- check_cache, return_cached_answer
|
| 49 |
+
- classify_intent
|
| 50 |
+
- search_stackoverflow, search_github, search_official_docs (๋ณ๋ ฌ)
|
| 51 |
+
- collect_results, evaluate_results, refine_search
|
| 52 |
+
- search_subgraph (์ค์ฒฉ ์๋ธ๊ทธ๋ํ: filter + summarize)
|
| 53 |
+
- generate_answer
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
### 2. ๋จ์ํ๋ ๋ฉ์ธ ๊ทธ๋ํ: `build_agent_graph()`
|
| 58 |
+
|
| 59 |
+
**๋ณ๊ฒฝ ์ (Phase 4)**: 60+ ๊ฐ์ ๋
ธ๋์ ์ฃ์ง๋ก ๋ณต์กํ๊ฒ ์ฝํ
|
| 60 |
+
|
| 61 |
+
**๋ณ๊ฒฝ ํ (Phase 5)**: 4๊ฐ์ ๋
ธ๋๋ง์ผ๋ก ๋จ์ํ
|
| 62 |
+
|
| 63 |
+
```python
|
| 64 |
+
def build_agent_graph() -> StateGraph:
|
| 65 |
+
"""
|
| 66 |
+
CodeWeaver ์์ด์ ํธ์ ๋ฉ์ธ ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
|
| 67 |
+
|
| 68 |
+
์ ์ฒด ํ๋ฆ (๋จ์ํ๋จ):
|
| 69 |
+
1. START โ create_plan (์ง๋ฌธ ์ ํ ๋ฐ ๊ฐ์ ํ๋จ)
|
| 70 |
+
2. ์ง๋ฌธ ์ ํ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ:
|
| 71 |
+
- single_topic: single_question_subgraph (1ํ) โ END
|
| 72 |
+
- multiple_questions: Send API๋ก single_question_subgraph (2ํ ๋ณ๋ ฌ) โ combine_answers โ END
|
| 73 |
+
- too_many: handle_too_many_questions โ END
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
graph = StateGraph(AgentState)
|
| 77 |
+
|
| 78 |
+
# ๋
ธ๋ ์ถ๊ฐ (4๊ฐ๋ง!)
|
| 79 |
+
graph.add_node("create_plan", create_plan_node)
|
| 80 |
+
graph.add_node("handle_too_many_questions", handle_too_many_questions_node)
|
| 81 |
+
graph.add_node("combine_answers", combine_answers_node)
|
| 82 |
+
graph.add_node("collect_subgraph_result", collect_subgraph_result_node)
|
| 83 |
+
|
| 84 |
+
# ์๋ธ๊ทธ๋ํ๋ฅผ ๋
ธ๋๋ก ๋ฑ๋ก
|
| 85 |
+
single_question_subgraph = build_single_question_subgraph()
|
| 86 |
+
graph.add_node("single_question_subgraph", single_question_subgraph)
|
| 87 |
+
|
| 88 |
+
# ๊ฐ๋จํ ์ฃ์ง ๊ตฌ์ฑ
|
| 89 |
+
graph.add_edge(START, "create_plan")
|
| 90 |
+
graph.add_conditional_edges("create_plan", route_after_plan)
|
| 91 |
+
graph.add_edge("handle_too_many_questions", END)
|
| 92 |
+
graph.add_conditional_edges("single_question_subgraph", ...)
|
| 93 |
+
graph.add_edge("collect_subgraph_result", "combine_answers")
|
| 94 |
+
graph.add_edge("combine_answers", END)
|
| 95 |
+
|
| 96 |
+
return graph
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
### 3. ๊ฐ์ ๋ ๋ผ์ฐํ
: `route_after_plan()`
|
| 102 |
+
|
| 103 |
+
**๋ณ๊ฒฝ ์ **: `initiate_dynamic_search` ๋
ธ๋ โ `fanout_multi_questions` ํจ์ โ `run_single_question_worker_node` โ ๋ด๋ถ์์ ๋ณ๋ ๊ทธ๋ํ ์คํ
|
| 104 |
+
|
| 105 |
+
**๋ณ๊ฒฝ ํ**: Send API๋ก ์๋ธ๊ทธ๋ํ๋ฅผ ์ง์ ํธ์ถ
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
def route_after_plan(state: AgentState):
|
| 109 |
+
"""
|
| 110 |
+
create_plan ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ค์ ๋
ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
- "handle_too_many_questions": ์ง๋ฌธ 3๊ฐ ์ด์
|
| 114 |
+
- "single_question_subgraph": ๋จ์ผ ์ฃผ์
|
| 115 |
+
- List[Send]: ๋ค์ค ์ง๋ฌธ (2๊ฐ) โ ์๋ธ๊ทธ๋ํ ๋ณ๋ ฌ ์คํ
|
| 116 |
+
"""
|
| 117 |
+
plan = state.plan or {}
|
| 118 |
+
case = plan.get("case", "single_topic")
|
| 119 |
+
|
| 120 |
+
if case == "too_many":
|
| 121 |
+
return "handle_too_many_questions"
|
| 122 |
+
elif case == "multiple_questions":
|
| 123 |
+
sub_questions = plan.get("sub_questions", [])
|
| 124 |
+
sends = []
|
| 125 |
+
for i, sq in enumerate(sub_questions):
|
| 126 |
+
child_state = state.model_copy(deep=True)
|
| 127 |
+
child_state.user_question = sq
|
| 128 |
+
child_state.is_multi_question = True
|
| 129 |
+
child_state.sub_question_index = i
|
| 130 |
+
# ... ์ต์ ํ๋ ์ค์ ...
|
| 131 |
+
sends.append(Send("single_question_subgraph", child_state))
|
| 132 |
+
return sends
|
| 133 |
+
else:
|
| 134 |
+
return "single_question_subgraph"
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
### 4. ์ ๊ฑฐ๋ ์ฝ๋ (300+ ์ค)
|
| 140 |
+
|
| 141 |
+
**ํ์ผ**: [`src/agent/nodes.py`](src/agent/nodes.py)
|
| 142 |
+
|
| 143 |
+
#### ์ ๊ฑฐ๋ ํจ์:
|
| 144 |
+
- โ `_build_search_subgraph_local()` - graph.py์ ๊ฒ ์ฌ์ฉ
|
| 145 |
+
- โ `_get_single_question_agent()` - ๊ณต์ ์๋ธ๊ทธ๋ํ๋ก ๋์ฒด (100+ ์ค)
|
| 146 |
+
- โ `run_single_question_worker_node()` - ๋ ์ด์ ํ์ ์์
|
| 147 |
+
- โ `initiate_dynamic_search_node()` - ๋จ์ ๋ถ๊ธฐ๋ก ๋์ฒด
|
| 148 |
+
- โ `fanout_multi_questions()` - route_after_plan์ ํตํฉ
|
| 149 |
+
|
| 150 |
+
#### ์ถ๊ฐ๋ ํจ์:
|
| 151 |
+
- โ
`collect_subgraph_result_node()` - ์๋ธ๊ทธ๋ํ ๊ฒฐ๊ณผ๋ฅผ multi_answers์ ์ถ๊ฐ
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## ์๋ก์ด ์ํคํ
์ฒ
|
| 156 |
+
|
| 157 |
+
```mermaid
|
| 158 |
+
graph TD
|
| 159 |
+
START[START] --> plan[create_plan]
|
| 160 |
+
|
| 161 |
+
plan -->|too_many| tooMany[handle_too_many_questions]
|
| 162 |
+
plan -->|single_topic| subgraph1[single_question_subgraph]
|
| 163 |
+
plan -->|multiple_2| fanout[Send API]
|
| 164 |
+
|
| 165 |
+
tooMany --> END
|
| 166 |
+
|
| 167 |
+
fanout -.Send Q1.-> subgraph2[single_question_subgraph]
|
| 168 |
+
fanout -.Send Q2.-> subgraph3[single_question_subgraph]
|
| 169 |
+
|
| 170 |
+
subgraph2 --> collect2[collect_subgraph_result]
|
| 171 |
+
subgraph3 --> collect3[collect_subgraph_result]
|
| 172 |
+
|
| 173 |
+
collect2 --> combine[combine_answers]
|
| 174 |
+
collect3 --> combine
|
| 175 |
+
|
| 176 |
+
combine --> END
|
| 177 |
+
subgraph1 --> END
|
| 178 |
+
|
| 179 |
+
subgraph SingleQuestionSubgraph
|
| 180 |
+
analyze[analyze_question] --> cache[check_cache]
|
| 181 |
+
cache --> classify[classify_intent]
|
| 182 |
+
classify --> search[Parallel Search]
|
| 183 |
+
search --> collect[collect_results]
|
| 184 |
+
collect --> eval[evaluate_results]
|
| 185 |
+
eval --> filter[search_subgraph]
|
| 186 |
+
filter --> generate[generate_answer]
|
| 187 |
+
end
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## ๊ฐ์ ํจ๊ณผ
|
| 193 |
+
|
| 194 |
+
### 1. ์ฝ๋ ํ์ง
|
| 195 |
+
- โ
**300+ ์ค ์ ๊ฑฐ**: ์ค๋ณต ๊ทธ๋ํ ๋น๋ ๋ก์ง ์์ ์ญ์
|
| 196 |
+
- โ
**์ฌ์ฌ์ฉ์ฑ ํฅ์**: ๋จ์ผ ์ง๋ฌธ ํ์ดํ๋ผ์ธ์ ๋
๋ฆฝ๋ ์๋ธ๊ทธ๋ํ๋ก ์บก์ํ
|
| 197 |
+
- โ
**์ ์ง๋ณด์์ฑ ํฅ์**: ์ญํ ๋ถ๋ฆฌ ๋ช
ํ (orchestration vs processing)
|
| 198 |
+
|
| 199 |
+
### 2. ๊ตฌ์กฐ ๋ช
ํํ
|
| 200 |
+
- **๋ถ๋ชจ ๊ทธ๋ํ (orchestration)**:
|
| 201 |
+
- ์ง๋ฌธ ์ ํ ํ๋จ
|
| 202 |
+
- ๋ถ๊ธฐ ๊ฒฐ์
|
| 203 |
+
- ๊ฒฐ๊ณผ ๋ณํฉ
|
| 204 |
+
|
| 205 |
+
- **์์ ์๋ธ๊ทธ๋ํ (processing)**:
|
| 206 |
+
- ์ง๋ฌธ ๋ถ์
|
| 207 |
+
- ์บ์ ํ์ธ
|
| 208 |
+
- ๊ฒ์ ์คํ
|
| 209 |
+
- ๋ต๋ณ ์์ฑ
|
| 210 |
+
|
| 211 |
+
### 3. ํ์ฅ์ฑ
|
| 212 |
+
- โ
์ง๋ฌธ 3๊ฐ ์ด์๋ ์ฝ๊ฒ ๋์ ๊ฐ๋ฅ (Send ๋ฆฌ์คํธ๋ง ํ์ฅ)
|
| 213 |
+
- โ
์๋ธ๊ทธ๋ํ ๋จ์๋ก ๋
๋ฆฝ ํ
์คํธ ๊ฐ๋ฅ
|
| 214 |
+
- โ
๋๋ฒ๊น
์ฉ์ด: ํน์ ์ง๋ฌธ ๋ฌธ์ ์ ํด๋น ์๋ธ๊ทธ๋ํ๋ง ํ์ธ
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## ๊ฒ์ฆ ๊ฒฐ๊ณผ
|
| 219 |
+
|
| 220 |
+
### ๊ตฌ์กฐ ๊ฒ์ฆ
|
| 221 |
+
```
|
| 222 |
+
============================================================
|
| 223 |
+
Phase 5: ์๋ธ๊ทธ๋ํ ๋ฆฌํฉํ ๋ง ๊ตฌ์กฐ ๊ฒ์ฆ
|
| 224 |
+
============================================================
|
| 225 |
+
โ
graph.py ๊ตฌ๋ฌธ ๊ฒ์ฆ ์ฑ๊ณต
|
| 226 |
+
|
| 227 |
+
[ํ์ ํจ์ ๊ฒ์ฆ]
|
| 228 |
+
โ
build_search_subgraph
|
| 229 |
+
โ
build_single_question_subgraph
|
| 230 |
+
โ
route_after_plan
|
| 231 |
+
โ
build_agent_graph
|
| 232 |
+
โ
create_agent
|
| 233 |
+
|
| 234 |
+
[์ ๊ฑฐ๋ ํจ์ ๊ฒ์ฆ]
|
| 235 |
+
โ
route_after_generate - ์ ์ ์ ๊ฑฐ๋จ
|
| 236 |
+
|
| 237 |
+
[Import ๊ฒ์ฆ]
|
| 238 |
+
โ
initiate_dynamic_search_node - import ์ ๊ฑฐ๋จ
|
| 239 |
+
โ
fanout_multi_questions - import ์ ๊ฑฐ๋จ
|
| 240 |
+
โ
run_single_question_worker_node - import ์ ๊ฑฐ๋จ
|
| 241 |
+
โ
collect_subgraph_result_node - import ์ถ๊ฐ๋จ
|
| 242 |
+
|
| 243 |
+
[๋ฉ์ธ ๊ทธ๋ํ ๋
ธ๋ ๊ฒ์ฆ]
|
| 244 |
+
โ
create_plan
|
| 245 |
+
โ
handle_too_many_questions
|
| 246 |
+
โ
combine_answers
|
| 247 |
+
โ
collect_subgraph_result
|
| 248 |
+
โ
single_question_subgraph
|
| 249 |
+
|
| 250 |
+
============================================================
|
| 251 |
+
nodes.py ๊ตฌ์กฐ ๊ฒ์ฆ
|
| 252 |
+
============================================================
|
| 253 |
+
โ
nodes.py ๊ตฌ๋ฌธ ๊ฒ์ฆ ์ฑ๊ณต
|
| 254 |
+
|
| 255 |
+
[์ ๊ฑฐ๋ ํจ์ ๊ฒ์ฆ]
|
| 256 |
+
โ
_build_search_subgraph_local - ์ ์ ์ ๊ฑฐ๋จ
|
| 257 |
+
โ
_get_single_question_agent - ์ ์ ์ ๊ฑฐ๋จ
|
| 258 |
+
โ
run_single_question_worker_node - ์ ์ ์ ๊ฑฐ๋จ
|
| 259 |
+
โ
initiate_dynamic_search_node - ์ ์ ์ ๊ฑฐ๋จ
|
| 260 |
+
โ
fanout_multi_questions - ์ ์ ์ ๊ฑฐ๋จ
|
| 261 |
+
|
| 262 |
+
[์ถ๊ฐ๋ ํจ์ ๊ฒ์ฆ]
|
| 263 |
+
โ
collect_subgraph_result_node
|
| 264 |
+
|
| 265 |
+
============================================================
|
| 266 |
+
๊ฒ์ฆ ๊ฒฐ๊ณผ ์์ฝ
|
| 267 |
+
============================================================
|
| 268 |
+
โ
์ฑ๊ณต: graph.py ๊ตฌ์กฐ
|
| 269 |
+
โ
์ฑ๊ณต: nodes.py ๊ตฌ์กฐ
|
| 270 |
+
|
| 271 |
+
๐ ๋ชจ๋ ๊ฒ์ฆ ํต๊ณผ! ๋ฆฌํฉํ ๋ง์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค.
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
---
|
| 275 |
+
|
| 276 |
+
## ๋ณ๊ฒฝ๋ ํ์ผ ๋ชฉ๋ก
|
| 277 |
+
|
| 278 |
+
1. **src/agent/graph.py**
|
| 279 |
+
- โ
`build_single_question_subgraph()` ์ถ๊ฐ (100+ ์ค)
|
| 280 |
+
- โ
`route_after_plan()` ๊ฐ์
|
| 281 |
+
- โ
`build_agent_graph()` ๋จ์ํ (200+ ์ค โ 50 ์ค)
|
| 282 |
+
- โ
`route_after_generate()` ์ ๊ฑฐ
|
| 283 |
+
- โ
Import ์ ๋ฆฌ
|
| 284 |
+
|
| 285 |
+
2. **src/agent/nodes.py**
|
| 286 |
+
- โ
`collect_subgraph_result_node()` ์ถ๊ฐ
|
| 287 |
+
- โ `_build_search_subgraph_local()` ์ ๊ฑฐ
|
| 288 |
+
- โ `_get_single_question_agent()` ์ ๊ฑฐ (100+ ์ค)
|
| 289 |
+
- โ `run_single_question_worker_node()` ์ ๊ฑฐ
|
| 290 |
+
- โ `initiate_dynamic_search_node()` ์ ๊ฑฐ
|
| 291 |
+
- โ `fanout_multi_questions()` ์ ๊ฑฐ
|
| 292 |
+
|
| 293 |
+
3. **hf-space/CodeWeaver/src/agent/**
|
| 294 |
+
- โ
graph.py ๋๊ธฐํ ์๋ฃ
|
| 295 |
+
- โ
nodes.py ๋๊ธฐํ ์๋ฃ
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
## ๋ค์ ๋จ๊ณ
|
| 300 |
+
|
| 301 |
+
์ด ๋ฆฌํฉํ ๋ง์ผ๋ก **Phase 5**๊ฐ ์๋ฃ๋์์ผ๋ฉฐ, ๋ค์ ๊ฐ์ ์ฌํญ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค:
|
| 302 |
+
|
| 303 |
+
1. **์ง๋ฌธ 3๊ฐ ์ด์ ์ง์**: `route_after_plan()`์์ Send ๋ฆฌ์คํธ๋ง ํ์ฅ
|
| 304 |
+
2. **์๋ธ๊ทธ๋ํ ๋จ์ ํ
์คํธ**: ๋
๋ฆฝ๋ ํ์ดํ๋ผ์ธ ๊ฒ์ฆ
|
| 305 |
+
3. **์บ์ฑ ์ ๋ต ๏ฟฝ๏ฟฝ๏ฟฝ์ **: ์๋ธ๊ทธ๋ํ ๊ฒฐ๊ณผ ์บ์ฑ
|
| 306 |
+
4. **์ฑ๋ฅ ์ต์ ํ**: ๋ณ๋ ฌ ์คํ ํจ์จ์ฑ ๋ถ์
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
## ๊ฒฐ๋ก
|
| 311 |
+
|
| 312 |
+
โ
**๋ชจ๋ ๋ชฉํ ๋ฌ์ฑ**
|
| 313 |
+
- ๋จ์ผ ์ง๋ฌธ ํ์ดํ๋ผ์ธ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์๋ธ๊ทธ๋ํ๋ก ์ถ์ถ
|
| 314 |
+
- ๋ถ๋ชจ ๊ทธ๋ํ๋ orchestration๋ง ๋ด๋น (4๊ฐ ๋
ธ๋)
|
| 315 |
+
- 300+ ์ค์ ์ค๋ณต ์ฝ๋ ์ ๊ฑฐ
|
| 316 |
+
- ๊ตฌ์กฐ ๋ช
ํํ ๋ฐ ํ์ฅ์ฑ ํฅ์
|
| 317 |
+
|
| 318 |
+
์ด ๋ฆฌํฉํ ๋ง์ผ๋ก CodeWeaver์ ์ํคํ
์ฒ๊ฐ **๋จ์ํ๊ณ **, **๋ช
ํํ๋ฉฐ**, **ํ์ฅ ๊ฐ๋ฅํ** ๊ตฌ์กฐ๋ก ๊ฐ์ ๋์์ต๋๋ค.
|
| 319 |
+
|
| 320 |
+
|
CodeWeaver/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CodeWeaver
|
| 3 |
+
emoji: ๐ค
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.1"
|
| 8 |
+
app_file: ui/app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# CodeWeaver
|
| 14 |
+
|
| 15 |
+
LangGraph ๊ธฐ๋ฐ์ **๊ฐ๋ฐ์ Q&A ์์ด์ ํธ**์
๋๋ค. ์ง๋ฌธ์ ๋ถ์ํ๊ณ (ํ์/๋
๋ฆฝ), **์บ์(Qdrant)**๋ฅผ ์ฐ์ ํ์ธํ ๋ค ์บ์ ๋ฏธ์ค์ผ ๋ **3๊ฐ ์์ค(Stack Overflow / GitHub / ๊ณต์ ๋ฌธ์(Tavily))๋ฅผ ๋ณ๋ ฌ ๊ฒ์**ํด ๋ต๋ณ์ ์์ฑํฉ๋๋ค. ์๋ก ๋
๋ฆฝ์ ์ธ ์ง๋ฌธ์ด 2๊ฐ ๋ค์ด์ค๋ฉด **๋์ ์ผ๋ก 2๊ฐ ํ์ดํ๋ผ์ธ์ ๋ณ๋ ฌ ์คํ**ํด ํตํฉ ๋ต๋ณ์ ์ ๊ณตํฉ๋๋ค.
|
| 16 |
+
|
| 17 |
+
## ํต์ฌ ๊ธฐ๋ฅ(ํ์ฌ ์ฝ๋ ๊ธฐ์ค)
|
| 18 |
+
|
| 19 |
+
- **์ง๋ฌธ ๊ฐ์ ๊ฐ์ง**: 1๊ฐ(๋จ์ผ ์ฃผ์ ) / 2๊ฐ(๋
๋ฆฝ ์ง๋ฌธ 2๊ฐ) / 3๊ฐ ์ด์(๊ฑฐ์ ์๋ด)
|
| 20 |
+
- **์ง๋ฌธ ํ์
๋ถ์**: `clarification`์ด๋ฉด ๊ฒ์/์บ์ ์์ด **๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ**
|
| 21 |
+
- **์๋ฏธ์ ์บ์ฑ**: Qdrant์ ์ง๋ฌธ-๋ต๋ณ์ ์ ์ฅํ๊ณ ์ ์ฌ ์ง๋ฌธ์ ๋น ๋ฅด๊ฒ ์ฌ์ฌ์ฉ(์๊ณ๊ฐ 0.85)
|
| 22 |
+
- **๋ณ๋ ฌ ๊ฒ์**: Stack Overflow / GitHub / Tavily(๊ณต์ ๋ฌธ์ ๋๋ฉ์ธ ์ ํ) ๋์ ๊ฒ์
|
| 23 |
+
- **๊ฒ์ ํ์ง ๋ณด์ **: ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํ๋ฉด **์ฟผ๋ฆฌ ๊ฐ์ ์ ์ต๋ 1ํ** ์ํ
|
| 24 |
+
- **์๋ธ๊ทธ๋ํ ์ฒ๋ฆฌ**: ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ํํฐ๋ง/์ ์ํ ํ ์์ฝ โ ์ต์ข
๋ต๋ณ ์์ฑ
|
| 25 |
+
|
| 26 |
+
## ๋ฌธ์
|
| 27 |
+
|
| 28 |
+
- ์ํคํ
์ฒ/๋์ ์๋ฆฌ: `../ARCHITECTURE.md`
|
| 29 |
+
- ๋ค์ค ์ง๋ฌธ ๋ณ๋ ฌ ์ฒ๋ฆฌ ์ค๊ณ(๋ฐฐ๊ฒฝ ์ค๋ช
): `../DYNAMIC_PARALLEL_SEARCH.md`
|
| 30 |
+
|
| 31 |
+
## ๋น ๋ฅธ ์์
|
| 32 |
+
|
| 33 |
+
### 1) ์ค์น
|
| 34 |
+
|
| 35 |
+
์๋๋ ์ ์ฅ์ ๋ฃจํธ๊ฐ ์๋๋ผ **`CodeWeaver/` ๋๋ ํฐ๋ฆฌ ๊ธฐ์ค** ์์์
๋๋ค.
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
cd CodeWeaver
|
| 39 |
+
|
| 40 |
+
# uv ์ฌ์ฉ(๊ถ์ฅ)
|
| 41 |
+
uv sync
|
| 42 |
+
|
| 43 |
+
# ๋๋ pip ์ฌ์ฉ
|
| 44 |
+
pip install -r requirements.txt
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
> `sentence-transformers`๊ฐ ์ต์ด ์คํ ์ `BAAI/bge-m3` ๋ชจ๋ธ์ ๋ค์ด๋ก๋ํ ์ ์์ต๋๋ค(๋คํธ์ํฌ ํ์).
|
| 48 |
+
|
| 49 |
+
### 2) ํ๊ฒฝ ๋ณ์ ์ค์ (.env)
|
| 50 |
+
|
| 51 |
+
`CodeWeaver/.env` ํ์ผ์ ๋ง๋ค๊ณ ์๋๋ฅผ ์ค์ ํ์ธ์(ํ์/์ ํ ๊ตฌ๋ถ).
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# ํ์: Gemini (LLM)
|
| 55 |
+
GOOGLE_API_KEY=your_google_api_key
|
| 56 |
+
|
| 57 |
+
# ํ์: Tavily (๊ณต์ ๋ฌธ์ ๊ฒ์)
|
| 58 |
+
TAVILY_API_KEY=your_tavily_api_key
|
| 59 |
+
|
| 60 |
+
# ํ์: Qdrant Cloud (์บ์)
|
| 61 |
+
QDRANT_URL=https://xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.us-east-1-0.aws.cloud.qdrant.io
|
| 62 |
+
QDRANT_API_KEY=your_qdrant_api_key
|
| 63 |
+
|
| 64 |
+
# ์ ํ: GitHub API rate limit ์ํ
|
| 65 |
+
GITHUB_TOKEN=your_github_token
|
| 66 |
+
|
| 67 |
+
# ์ ํ: LangSmith ํธ๋ ์ด์ฑ
|
| 68 |
+
LANGCHAIN_TRACING_V2=true
|
| 69 |
+
LANGCHAIN_API_KEY=your_langsmith_api_key
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### 3) ์คํ(Gradio UI)
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
cd CodeWeaver
|
| 76 |
+
python ui/app.py
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
๊ธฐ๋ณธ ์ฃผ์: `http://localhost:7860`
|
| 80 |
+
|
| 81 |
+
## ํ์ฌ ํด๋ ๊ตฌ์กฐ
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
CodeWeaver/
|
| 85 |
+
โโโ main.py
|
| 86 |
+
โโโ pyproject.toml
|
| 87 |
+
โโโ requirements.txt
|
| 88 |
+
โโโ src/
|
| 89 |
+
โ โโโ agent/
|
| 90 |
+
โ โ โโโ graph.py # LangGraph ๋ฉ์ธ ๊ทธ๋ํ(๋ผ์ฐํ
/๋ณ๋ ฌํ)
|
| 91 |
+
โ โ โโโ nodes.py # ๊ฐ ๋
ธ๋ ๊ตฌํ
|
| 92 |
+
โ โ โโโ state.py # AgentState + reducer ์ ์
|
| 93 |
+
โ โโโ tools/
|
| 94 |
+
โ โ โโโ search_tools.py # StackOverflow/GitHub/Tavily ๊ฒ์
|
| 95 |
+
โ โโโ utils/
|
| 96 |
+
โ โ โโโ tracing.py # trace_node ๋ฐ์ฝ๋ ์ดํฐ(LangSmith ์ฐ๋)
|
| 97 |
+
โ โโโ vector_db/
|
| 98 |
+
โ โโโ qdrant_client.py # Qdrant ์บ์ ๊ด๋ฆฌ
|
| 99 |
+
โ โโโ local_embeddings.py # bge-m3 ๋ก์ปฌ ์๋ฒ ๋ฉ
|
| 100 |
+
โโโ ui/
|
| 101 |
+
โโโ app.py # Gradio UI (์ค์ ์ํธ๋ฆฌ)
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## ๋์ ํ๋ฆ(์์ฝ)
|
| 105 |
+
|
| 106 |
+
- `START โ create_plan`
|
| 107 |
+
- **3๊ฐ ์ด์**์ด๋ฉด ์๋ด ๋ฉ์์ง ๋ฐํ
|
| 108 |
+
- **2๊ฐ**๋ฉด ๊ฐ ์ง๋ฌธ์ worker์์ ๋จ์ผ ํ์ดํ๋ผ์ธ์ผ๋ก ์คํ ํ ๊ฒฐํฉ
|
| 109 |
+
- **1๊ฐ**๋ฉด ์๋ ๋จ์ผ ํ์ดํ๋ผ์ธ ์ํ
|
| 110 |
+
- ๋จ์ผ ํ์ดํ๋ผ์ธ:
|
| 111 |
+
- `analyze_question`
|
| 112 |
+
- `clarification`์ด๋ฉด `generate_with_history`๋ก ์ฆ์ ๋ต๋ณ
|
| 113 |
+
- ๊ทธ ์ธ: `check_cache` โ hit๋ฉด ๋ฐํ, miss๋ฉด `classify_intent`
|
| 114 |
+
- `classify_intent` โ 3์์ค ๋ณ๋ ฌ ๊ฒ์ โ `collect_results` โ `evaluate_results`
|
| 115 |
+
- ํ์ ์ `refine_search` 1ํ โ ์ฌ๊ฒ์
|
| 116 |
+
- `filter_and_score โ summarize_results โ generate_answer`(+์กฐ๊ฑด๋ถ ์บ์ ์ ์ฅ)
|
| 117 |
+
|
| 118 |
+
์์ธํ ์๋ฆฌ๋ `../ARCHITECTURE.md`๋ฅผ ์ฐธ๊ณ ํ์ธ์.
|
CodeWeaver/main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from codeweaver!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
CodeWeaver/pyproject.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "codeweaver"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"qdrant-client",
|
| 9 |
+
"pytest",
|
| 10 |
+
"pytest-asyncio",
|
| 11 |
+
"python-dotenv",
|
| 12 |
+
"tavily-python",
|
| 13 |
+
"requests",
|
| 14 |
+
"langsmith>=0.1.0",
|
| 15 |
+
"langchain-core>=0.3.0",
|
| 16 |
+
"langchain-google-genai>=2.0.0",
|
| 17 |
+
"langgraph>=0.2.0",
|
| 18 |
+
"sentence-transformers>=3.0.0",
|
| 19 |
+
"torch>=2.0.0",
|
| 20 |
+
"gradio==4.44.1",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[tool.pytest.ini_options]
|
| 24 |
+
pythonpath = ["."]
|
| 25 |
+
markers = [
|
| 26 |
+
"slow: ์ค์ API ํธ์ถ์ด ํ์ํ ๋๋ฆฐ ํ
์คํธ (--slow ์ต์
์ผ๋ก ์คํ)",
|
| 27 |
+
]
|
CodeWeaver/requirements.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LangGraph & LangChain
|
| 2 |
+
langgraph>=0.2.0
|
| 3 |
+
langchain-google-genai>=2.0.0
|
| 4 |
+
langchain-core>=0.3.0
|
| 5 |
+
langsmith>=0.2.0
|
| 6 |
+
|
| 7 |
+
# Vector DB
|
| 8 |
+
qdrant-client>=1.11.0
|
| 9 |
+
|
| 10 |
+
# Search APIs
|
| 11 |
+
tavily-python>=0.5.0
|
| 12 |
+
requests>=2.31.0
|
| 13 |
+
|
| 14 |
+
# Embeddings
|
| 15 |
+
sentence-transformers>=3.0.0
|
| 16 |
+
torch>=2.0.0
|
| 17 |
+
|
| 18 |
+
# UI
|
| 19 |
+
gradio==4.44.1
|
| 20 |
+
|
| 21 |
+
# Utils
|
| 22 |
+
python-dotenv>=1.0.0
|
| 23 |
+
pydantic>=2.0.0
|
| 24 |
+
|
CodeWeaver/src/__init__.py
ADDED
|
File without changes
|
CodeWeaver/src/agent/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeWeaver ์์ด์ ํธ ๋ชจ๋.
|
| 3 |
+
|
| 4 |
+
LangGraph ๊ธฐ๋ฐ ๊ฐ๋ฐ์ ์ง๋ฌธ ๋ต๋ณ ์์ด์ ํธ๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 5 |
+
|
| 6 |
+
์ฃผ์ ์ปดํฌ๋ํธ:
|
| 7 |
+
- State: ์์ด์ ํธ ์ํ ๊ด๋ฆฌ
|
| 8 |
+
- Nodes: ๊ฐ๋ณ ์ฒ๋ฆฌ ๋
ธ๋
|
| 9 |
+
- Graph: LangGraph ์ํฌํ๋ก์ฐ
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from .state import AgentState, SearchResult
|
| 13 |
+
from .graph import agent, build_agent_graph, create_agent
|
| 14 |
+
from .nodes import (
|
| 15 |
+
analyze_question_node,
|
| 16 |
+
check_cache_node,
|
| 17 |
+
classify_intent_node,
|
| 18 |
+
search_stackoverflow_node,
|
| 19 |
+
search_github_node,
|
| 20 |
+
search_official_docs_node,
|
| 21 |
+
filter_and_score_node,
|
| 22 |
+
summarize_results_node,
|
| 23 |
+
generate_answer_node,
|
| 24 |
+
return_cached_answer_node,
|
| 25 |
+
generate_with_history_node,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
__all__ = [
|
| 29 |
+
# State
|
| 30 |
+
"AgentState",
|
| 31 |
+
"SearchResult",
|
| 32 |
+
|
| 33 |
+
# Graph
|
| 34 |
+
"agent",
|
| 35 |
+
"build_agent_graph",
|
| 36 |
+
"create_agent",
|
| 37 |
+
|
| 38 |
+
# Nodes
|
| 39 |
+
"analyze_question_node",
|
| 40 |
+
"check_cache_node",
|
| 41 |
+
"classify_intent_node",
|
| 42 |
+
"search_stackoverflow_node",
|
| 43 |
+
"search_github_node",
|
| 44 |
+
"search_official_docs_node",
|
| 45 |
+
"filter_and_score_node",
|
| 46 |
+
"summarize_results_node",
|
| 47 |
+
"generate_answer_node",
|
| 48 |
+
"return_cached_answer_node",
|
| 49 |
+
"generate_with_history_node",
|
| 50 |
+
]
|
| 51 |
+
|
CodeWeaver/src/agent/graph.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeWeaver LangGraph ์ํฌํ๋ก์ฐ ๊ตฌ์ฑ.
|
| 3 |
+
|
| 4 |
+
LangGraph 6๊ฐ์ง ํต์ฌ ๊ธฐ๋ฅ ์๋ฒฝ ๊ตฌํ:
|
| 5 |
+
โ
Conditional Edges: ์ง๋ฌธ ์ ํ, ์บ์ ์ฌ๋ถ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ
|
| 6 |
+
โ
Send API: 3๊ฐ ๊ฒ์ ๋
ธ๋ ๋ณ๋ ฌ ์คํ (fan-out/fan-in)
|
| 7 |
+
โ
Subgraph: ๋จ์ผ ์ง๋ฌธ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ + ๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ
|
| 8 |
+
โ
Map-Reduce: Send API๋ก ๋ณ๋ ฌ ๊ฒ์ โ ๊ฒฐ๊ณผ ๋จธ์ง
|
| 9 |
+
โ
Checkpointing: MemorySaver๋ก ๋ํ ์ํ ์ ์ฅ
|
| 10 |
+
โ
Pydantic Typed State: ํ์
์์ ์ฑ ๋ณด์ฅ
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Literal
|
| 15 |
+
|
| 16 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 17 |
+
from langgraph.graph import StateGraph, START, END
|
| 18 |
+
from langgraph.types import Send
|
| 19 |
+
|
| 20 |
+
from src.agent.state import AgentState, WorkerState, _MULTI_ANS_RESET_TOKEN
|
| 21 |
+
from src.agent.nodes import (
|
| 22 |
+
analyze_question_node,
|
| 23 |
+
check_cache_node,
|
| 24 |
+
create_plan_node,
|
| 25 |
+
classify_intent_node,
|
| 26 |
+
search_stackoverflow_node,
|
| 27 |
+
search_github_node,
|
| 28 |
+
search_official_docs_node,
|
| 29 |
+
collect_results_node,
|
| 30 |
+
evaluate_results_node,
|
| 31 |
+
refine_search_node,
|
| 32 |
+
filter_and_score_node,
|
| 33 |
+
summarize_results_node,
|
| 34 |
+
generate_answer_node,
|
| 35 |
+
return_cached_answer_node,
|
| 36 |
+
generate_with_history_node,
|
| 37 |
+
handle_too_many_questions_node,
|
| 38 |
+
combine_answers_node,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def build_search_subgraph() -> StateGraph:
|
| 45 |
+
"""
|
| 46 |
+
๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ์๋ธ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
|
| 47 |
+
|
| 48 |
+
ํ๋ฆ: filter_and_score โ summarize_results
|
| 49 |
+
|
| 50 |
+
์ด ์๋ธ๊ทธ๋ํ๋ single_question_subgraph ๋ด๋ถ์์ ์ฌ์ฉ๋๋ฏ๋ก
|
| 51 |
+
WorkerState๋ฅผ ์ฌ์ฉํ์ฌ ์ฑ๋ ํ์
์ถฉ๋์ ๋ฐฉ์งํฉ๋๋ค.
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
์ปดํ์ผ๋ ์๋ธ๊ทธ๋ํ
|
| 55 |
+
"""
|
| 56 |
+
# ์๋ธ๊ทธ๋ํ ์์ฑ (WorkerState ์ฌ์ฉ)
|
| 57 |
+
subgraph = StateGraph(WorkerState)
|
| 58 |
+
|
| 59 |
+
# ๋
ธ๋ ์ถ๊ฐ
|
| 60 |
+
subgraph.add_node("filter_and_score", filter_and_score_node)
|
| 61 |
+
subgraph.add_node("summarize_results", summarize_results_node)
|
| 62 |
+
|
| 63 |
+
# ์๋ธ๊ทธ๋ํ ๋ด๋ถ ํ๋ฆ ์ ์
|
| 64 |
+
# START โ filter_and_score โ summarize_results โ END
|
| 65 |
+
subgraph.add_edge(START, "filter_and_score")
|
| 66 |
+
subgraph.add_edge("filter_and_score", "summarize_results")
|
| 67 |
+
subgraph.add_edge("summarize_results", END)
|
| 68 |
+
|
| 69 |
+
return subgraph.compile()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def route_after_analysis_worker(state: WorkerState) -> Literal["generate_with_history", "check_cache"]:
|
| 73 |
+
"""
|
| 74 |
+
์ง๋ฌธ ๋ถ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ค์ ๋
ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค (WorkerState์ฉ).
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
state: ํ์ฌ ์์ปค ์ํ
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
- "generate_with_history": ํ์ ์ง๋ฌธ โ ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ
|
| 81 |
+
- "check_cache": ๋
๋ฆฝ ์ง๋ฌธ โ ์บ์ ํ์ธ
|
| 82 |
+
"""
|
| 83 |
+
raw_qtype = state.question_type or "independent"
|
| 84 |
+
legacy_map = {
|
| 85 |
+
"followup": "clarification",
|
| 86 |
+
"cache_candidate": "independent",
|
| 87 |
+
"new_search": "independent",
|
| 88 |
+
}
|
| 89 |
+
question_type = legacy_map.get(raw_qtype, raw_qtype)
|
| 90 |
+
|
| 91 |
+
if question_type == "clarification":
|
| 92 |
+
return "generate_with_history"
|
| 93 |
+
|
| 94 |
+
return "check_cache"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def route_after_cache_worker(state: WorkerState) -> Literal["return_cached_answer", "classify_intent"]:
|
| 98 |
+
"""
|
| 99 |
+
์บ์ ํํธ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ค์ ๋
ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค (WorkerState์ฉ).
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
state: ํ์ฌ ์์ปค ์ํ
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
- "return_cached_answer": ์บ์ ํํธ ์ ์ฆ์ ๋ต๋ณ ๋ฐํ
|
| 106 |
+
- "classify_intent": ์บ์ ๋ฏธ์ค ์ ์๋ ๋ถ๋ฅ
|
| 107 |
+
"""
|
| 108 |
+
if state.cached_result:
|
| 109 |
+
return "return_cached_answer"
|
| 110 |
+
else:
|
| 111 |
+
return "classify_intent"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def route_after_evaluation_worker(state: WorkerState) -> Literal["refine_search", "search_subgraph"]:
|
| 115 |
+
"""
|
| 116 |
+
๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ ํ ๋ค์ ๋
ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค (WorkerState์ฉ).
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
state: ํ์ฌ ์์ปค ์ํ
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
- "refine_search": ๊ฒฐ๊ณผ ๋ถ์กฑ & ๊ฐ์ ํ์ 0ํ โ ์ฟผ๋ฆฌ ๊ฐ์
|
| 123 |
+
- "search_subgraph": ๊ฒฐ๊ณผ ์ถฉ๋ถ or ๊ฐ์ ํ์ 1ํ โ ํํฐ๋ง ์งํ
|
| 124 |
+
"""
|
| 125 |
+
needs_refinement = state.needs_refinement
|
| 126 |
+
refinement_count = state.refinement_count
|
| 127 |
+
|
| 128 |
+
if needs_refinement and refinement_count < 1:
|
| 129 |
+
return "refine_search"
|
| 130 |
+
else:
|
| 131 |
+
return "search_subgraph"
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def initiate_parallel_search_worker(state: WorkerState):
|
| 135 |
+
"""
|
| 136 |
+
Send API๋ฅผ ์ฌ์ฉํ์ฌ 3๊ฐ์ ๊ฒ์ ๋
ธ๋๋ฅผ ๋ณ๋ ฌ๋ก ์คํํฉ๋๋ค (WorkerState์ฉ).
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
state: ํ์ฌ ์์ปค ์ํ
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
Send ๊ฐ์ฒด ๋ฆฌ์คํธ (fan-out)
|
| 143 |
+
"""
|
| 144 |
+
return [
|
| 145 |
+
Send("search_stackoverflow", state),
|
| 146 |
+
Send("search_github", state),
|
| 147 |
+
Send("search_official_docs", state),
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def build_single_question_subgraph() -> StateGraph:
|
| 152 |
+
"""
|
| 153 |
+
๋จ์ผ ์ง๋ฌธ ์ฒ๋ฆฌ ์๋ธ๊ทธ๋ํ.
|
| 154 |
+
|
| 155 |
+
๐ง CRITICAL:
|
| 156 |
+
- WorkerState๋ง ์ฌ์ฉ
|
| 157 |
+
- ๋ถ๋ชจ AgentState์ ์์ ํ ๊ฒฉ๋ฆฌ
|
| 158 |
+
- ์ถ๋ ฅ: multi_answers ๋๋ final_answer๋ง
|
| 159 |
+
"""
|
| 160 |
+
# WorkerState ์ฌ์ฉ (AgentState์ ์์ ํ ๋
๋ฆฝ)
|
| 161 |
+
subgraph = StateGraph(WorkerState)
|
| 162 |
+
|
| 163 |
+
# ๋
ธ๋ ์ถ๊ฐ
|
| 164 |
+
subgraph.add_node("analyze_question", analyze_question_node)
|
| 165 |
+
subgraph.add_node("generate_with_history", generate_with_history_node)
|
| 166 |
+
subgraph.add_node("check_cache", check_cache_node)
|
| 167 |
+
subgraph.add_node("return_cached_answer", return_cached_answer_node)
|
| 168 |
+
subgraph.add_node("classify_intent", classify_intent_node)
|
| 169 |
+
|
| 170 |
+
# ๋ณ๋ ฌ ๊ฒ์ ๋
ธ๋
|
| 171 |
+
subgraph.add_node("search_stackoverflow", search_stackoverflow_node)
|
| 172 |
+
subgraph.add_node("search_github", search_github_node)
|
| 173 |
+
subgraph.add_node("search_official_docs", search_official_docs_node)
|
| 174 |
+
|
| 175 |
+
# ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ๋
ธ๋
|
| 176 |
+
subgraph.add_node("collect_results", collect_results_node)
|
| 177 |
+
subgraph.add_node("evaluate_results", evaluate_results_node)
|
| 178 |
+
subgraph.add_node("refine_search", refine_search_node)
|
| 179 |
+
|
| 180 |
+
# ์ต์ข
๋ต๋ณ ์์ฑ
|
| 181 |
+
subgraph.add_node("generate_answer", generate_answer_node)
|
| 182 |
+
|
| 183 |
+
# ์ค์ฒฉ ์๋ธ๊ทธ๋ํ (filter + summarize)
|
| 184 |
+
filter_summarize_subgraph = build_search_subgraph()
|
| 185 |
+
subgraph.add_node("search_subgraph", filter_summarize_subgraph)
|
| 186 |
+
|
| 187 |
+
# ===== ์ฃ์ง ๊ตฌ์ฑ =====
|
| 188 |
+
|
| 189 |
+
# 1. START โ analyze_question
|
| 190 |
+
subgraph.add_edge(START, "analyze_question")
|
| 191 |
+
|
| 192 |
+
# 2. analyze_question ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ
|
| 193 |
+
subgraph.add_conditional_edges(
|
| 194 |
+
"analyze_question",
|
| 195 |
+
route_after_analysis_worker,
|
| 196 |
+
{
|
| 197 |
+
"generate_with_history": "generate_with_history",
|
| 198 |
+
"check_cache": "check_cache",
|
| 199 |
+
}
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
# 3. generate_with_history โ END (๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ)
|
| 203 |
+
subgraph.add_edge("generate_with_history", END)
|
| 204 |
+
|
| 205 |
+
# 4. check_cache ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ
|
| 206 |
+
subgraph.add_conditional_edges(
|
| 207 |
+
"check_cache",
|
| 208 |
+
route_after_cache_worker,
|
| 209 |
+
{
|
| 210 |
+
"return_cached_answer": "return_cached_answer",
|
| 211 |
+
"classify_intent": "classify_intent",
|
| 212 |
+
}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# 5. return_cached_answer โ END (์บ์ ํํธ)
|
| 216 |
+
subgraph.add_edge("return_cached_answer", END)
|
| 217 |
+
|
| 218 |
+
# 6. classify_intent โ ๋ณ๋ ฌ ๊ฒ์ (Send API)
|
| 219 |
+
subgraph.add_conditional_edges("classify_intent", initiate_parallel_search_worker)
|
| 220 |
+
|
| 221 |
+
# 7. ๋ชจ๋ ๊ฒ์ ๋
ธ๋ โ collect_results (fan-in)
|
| 222 |
+
subgraph.add_edge("search_stackoverflow", "collect_results")
|
| 223 |
+
subgraph.add_edge("search_github", "collect_results")
|
| 224 |
+
subgraph.add_edge("search_official_docs", "collect_results")
|
| 225 |
+
|
| 226 |
+
# 8. collect_results โ evaluate_results
|
| 227 |
+
subgraph.add_edge("collect_results", "evaluate_results")
|
| 228 |
+
|
| 229 |
+
# 9. evaluate_results ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ
|
| 230 |
+
subgraph.add_conditional_edges(
|
| 231 |
+
"evaluate_results",
|
| 232 |
+
route_after_evaluation_worker,
|
| 233 |
+
{
|
| 234 |
+
"refine_search": "refine_search",
|
| 235 |
+
"search_subgraph": "search_subgraph",
|
| 236 |
+
}
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# 10. refine_search โ classify_intent (์ฟผ๋ฆฌ ๊ฐ์ ๋ฃจํ)
|
| 240 |
+
subgraph.add_edge("refine_search", "classify_intent")
|
| 241 |
+
|
| 242 |
+
# 11. search_subgraph โ generate_answer
|
| 243 |
+
subgraph.add_edge("search_subgraph", "generate_answer")
|
| 244 |
+
|
| 245 |
+
# 12. generate_answer โ END
|
| 246 |
+
subgraph.add_edge("generate_answer", END)
|
| 247 |
+
|
| 248 |
+
return subgraph.compile()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def route_after_plan(state: AgentState):
|
| 252 |
+
"""
|
| 253 |
+
create_plan ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ค์ ๋
ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
|
| 254 |
+
|
| 255 |
+
Returns:
|
| 256 |
+
- "handle_too_many_questions": ์ง๋ฌธ 3๊ฐ ์ด์
|
| 257 |
+
- "single_question_subgraph": ๋จ์ผ ์ฃผ์ (1ํ ์คํ)
|
| 258 |
+
- List[Send]: ๋ค์ค ์ง๋ฌธ (Nํ ๋ณ๋ ฌ ์คํ)
|
| 259 |
+
"""
|
| 260 |
+
plan = state.plan or {}
|
| 261 |
+
case = plan.get("case", "single_topic")
|
| 262 |
+
|
| 263 |
+
if case == "too_many":
|
| 264 |
+
return "handle_too_many_questions"
|
| 265 |
+
|
| 266 |
+
elif case == "multiple_questions":
|
| 267 |
+
# ๋ค์ค ์ง๋ฌธ: Send API๋ก ์๋ธ๊ทธ๋ํ๋ฅผ ์ฌ๋ฌ ๋ฒ ํธ์ถ
|
| 268 |
+
sub_questions = plan.get("sub_questions", [])
|
| 269 |
+
messages = state.messages
|
| 270 |
+
|
| 271 |
+
logger.info("๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ: %d๊ฐ ์ง๋ฌธ์ ์๋ธ๊ทธ๋ํ๋ก ๋ณ๋ ฌ ์คํ", len(sub_questions))
|
| 272 |
+
|
| 273 |
+
sends = []
|
| 274 |
+
for i, sq in enumerate(sub_questions):
|
| 275 |
+
worker_state = WorkerState(
|
| 276 |
+
processing_question=sq,
|
| 277 |
+
messages=messages,
|
| 278 |
+
|
| 279 |
+
# ๐ง [FIX] ์ด๋ฆ ๋ณ๊ฒฝ๋ ํ๋๋ก ๋งคํ
|
| 280 |
+
worker_is_multi=True,
|
| 281 |
+
worker_idx=i,
|
| 282 |
+
worker_sub_text=sq,
|
| 283 |
+
)
|
| 284 |
+
sends.append(Send("single_question_subgraph", worker_state))
|
| 285 |
+
|
| 286 |
+
return sends
|
| 287 |
+
|
| 288 |
+
else:
|
| 289 |
+
# ๋จ์ผ ์ง๋ฌธ
|
| 290 |
+
worker_state = WorkerState(
|
| 291 |
+
processing_question=state.user_question,
|
| 292 |
+
messages=state.messages,
|
| 293 |
+
|
| 294 |
+
# ๐ง [FIX] ๊ธฐ๋ณธ๊ฐ ๋งคํ
|
| 295 |
+
worker_is_multi=False,
|
| 296 |
+
worker_idx=0,
|
| 297 |
+
worker_sub_text=None
|
| 298 |
+
)
|
| 299 |
+
return [Send("single_question_subgraph", worker_state)]
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def route_after_subgraph(state: AgentState) -> Literal["combine_answers", END]:
|
| 303 |
+
"""
|
| 304 |
+
์๋ธ๊ทธ๋ํ ์คํ ํ ๋ค์ ๋
ธ๋ ๊ฒฐ์ .
|
| 305 |
+
|
| 306 |
+
- multi_answers๊ฐ ์์ผ๋ฉด: ๋ค์ค ์ง๋ฌธ ๋ชจ๋ โ combine_answers
|
| 307 |
+
- multi_answers๊ฐ ์์ผ๋ฉด: ๋จ์ผ ์ง๋ฌธ ๋ชจ๋ โ END
|
| 308 |
+
"""
|
| 309 |
+
# multi_answers์ ์ค์ ๋ฐ์ดํฐ๊ฐ ์๋์ง ํ์ธ (reset token ์ ์ธ)
|
| 310 |
+
has_answers = any(
|
| 311 |
+
isinstance(item, dict) and item.get("__token__") != _MULTI_ANS_RESET_TOKEN
|
| 312 |
+
for item in state.multi_answers
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
if has_answers:
|
| 316 |
+
logger.info("๋ค์ค ์ง๋ฌธ ๋ชจ๋: combine_answers๋ก ์ด๋")
|
| 317 |
+
return "combine_answers"
|
| 318 |
+
else:
|
| 319 |
+
logger.info("๋จ์ผ ์ง๋ฌธ ๋ชจ๋: END๋ก ์ด๋")
|
| 320 |
+
return END
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def build_agent_graph() -> StateGraph:
|
| 324 |
+
"""
|
| 325 |
+
CodeWeaver ์์ด์ ํธ์ ๋ฉ์ธ ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
|
| 326 |
+
|
| 327 |
+
์ ์ฒด ํ๋ฆ (๋จ์ํ๋จ):
|
| 328 |
+
1. START โ create_plan (์ง๋ฌธ ์ ํ ๋ฐ ๊ฐ์ ํ๋จ)
|
| 329 |
+
2. ์ง๋ฌธ ์ ํ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ:
|
| 330 |
+
- single_topic: single_question_subgraph (1ํ) โ END
|
| 331 |
+
- multiple_questions: Send API๋ก single_question_subgraph (2ํ ๋ณ๋ ฌ) โ combine_answers โ END
|
| 332 |
+
- too_many: handle_too_many_questions โ END
|
| 333 |
+
|
| 334 |
+
ํต์ฌ ๊ฐ์ ์ฌํญ:
|
| 335 |
+
- โ
๋จ์ผ ์ง๋ฌธ ํ์ดํ๋ผ์ธ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์๋ธ๊ทธ๋ํ๋ก ์ถ์ถ
|
| 336 |
+
- โ
๋ถ๋ชจ ๊ทธ๋ํ๋ ๊ณํ/๋ถ๊ธฐ/๋ณํฉ๋ง ๋ด๋น
|
| 337 |
+
- โ
๋ณต์กํ worker ๋
ธ๋ ์ ๊ฑฐ
|
| 338 |
+
- โ
์ฝ๋ ์ค๋ณต ์ ๊ฑฐ
|
| 339 |
+
- โ
๊ตฌ์กฐ ๋ช
ํํ: ๋ถ๋ชจ(orchestration) vs ์์(processing)
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
๊ตฌ์ฑ๋ StateGraph (์ปดํ์ผ ์ )
|
| 343 |
+
"""
|
| 344 |
+
# ๋ฉ์ธ ๊ทธ๋ํ ์์ฑ
|
| 345 |
+
graph = StateGraph(AgentState)
|
| 346 |
+
|
| 347 |
+
# ๋
ธ๋ ์ถ๊ฐ
|
| 348 |
+
graph.add_node("create_plan", create_plan_node)
|
| 349 |
+
graph.add_node("handle_too_many_questions", handle_too_many_questions_node)
|
| 350 |
+
graph.add_node("combine_answers", combine_answers_node)
|
| 351 |
+
|
| 352 |
+
# ์๋ธ๊ทธ๋ํ๋ฅผ ๋
ธ๋๋ก ๋ฑ๋ก
|
| 353 |
+
single_question_subgraph = build_single_question_subgraph()
|
| 354 |
+
graph.add_node("single_question_subgraph", single_question_subgraph)
|
| 355 |
+
|
| 356 |
+
# ===== ์ฃ์ง ๊ตฌ์ฑ =====
|
| 357 |
+
|
| 358 |
+
# 1. START โ create_plan
|
| 359 |
+
graph.add_edge(START, "create_plan")
|
| 360 |
+
|
| 361 |
+
# 2. create_plan โ ๋ถ๊ธฐ
|
| 362 |
+
# - single_topic: "single_question_subgraph" โ END
|
| 363 |
+
# - multiple_questions: List[Send("single_question_subgraph", WorkerState)] โ combine_answers
|
| 364 |
+
# - too_many: "handle_too_many_questions" โ END
|
| 365 |
+
graph.add_conditional_edges("create_plan", route_after_plan)
|
| 366 |
+
|
| 367 |
+
# 3. handle_too_many_questions โ END
|
| 368 |
+
graph.add_edge("handle_too_many_questions", END)
|
| 369 |
+
|
| 370 |
+
# 4. ๐ง FIX: single_question_subgraph์ ์ถ๊ตฌ๋ฅผ ๋ช
ํํ ๋ถ๋ฆฌ
|
| 371 |
+
# - ๋จ์ผ ์ง๋ฌธ (case=single_topic): ๋ฌด์กฐ๊ฑด END
|
| 372 |
+
# - ๋ค์ค ์ง๋ฌธ (case=multiple_questions): Send API๊ฐ ์๋์ผ๋ก combine_answers๋ก fan-in
|
| 373 |
+
|
| 374 |
+
# 4-1. ๋จ์ผ ์ง๋ฌธ ๊ฒฝ๋ก: single_question_subgraph โ END
|
| 375 |
+
# 4-2. ๋ค์ค ์ง๋ฌธ ๊ฒฝ๋ก: single_question_subgraph โ combine_answers (์๋ fan-in)
|
| 376 |
+
|
| 377 |
+
# ๐ง ํด๊ฒฐ์ฑ
: conditional edges๋ก ๋ถ๊ธฐ
|
| 378 |
+
graph.add_conditional_edges(
|
| 379 |
+
"single_question_subgraph",
|
| 380 |
+
route_after_subgraph,
|
| 381 |
+
{
|
| 382 |
+
"combine_answers": "combine_answers",
|
| 383 |
+
END: END,
|
| 384 |
+
}
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# 5. combine_answers โ END
|
| 388 |
+
graph.add_edge("combine_answers", END)
|
| 389 |
+
|
| 390 |
+
return graph
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def create_agent(enable_checkpointing: bool = True):
|
| 394 |
+
"""
|
| 395 |
+
CodeWeaver ์์ด์ ํธ๋ฅผ ์์ฑํ๊ณ ์ปดํ์ผํฉ๋๋ค.
|
| 396 |
+
|
| 397 |
+
Args:
|
| 398 |
+
enable_checkpointing: ์ฒดํฌํฌ์ธํธ ํ์ฑํ ์ฌ๋ถ
|
| 399 |
+
- True: MemorySaver ์ฌ์ฉ (๊ฐ๋ฐ/ํ
์คํธ์ฉ)
|
| 400 |
+
- False: ์ฒดํฌํฌ์ธํธ ์์ด ์คํ (์ํ ์ ์ฅ ๋ถ๊ฐ)
|
| 401 |
+
|
| 402 |
+
Returns:
|
| 403 |
+
์ปดํ์ผ๋ ์คํ ๊ฐ๋ฅํ ๊ทธ๋ํ
|
| 404 |
+
|
| 405 |
+
Note:
|
| 406 |
+
ํ๋ก๋์
ํ๊ฒฝ์์๋ MemorySaver ๋์
|
| 407 |
+
PostgresSaver, SqliteSaver ๋ฑ ์๊ตฌ ์ ์ฅ์ ์ฌ์ฉ ๊ถ์ฅ
|
| 408 |
+
"""
|
| 409 |
+
graph = build_agent_graph()
|
| 410 |
+
|
| 411 |
+
if enable_checkpointing:
|
| 412 |
+
# ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ ์ฒดํฌํฌ์ธํฐ (ํ๋ก๋์
์์๋ DB ์ฌ์ฉ ๊ถ์ฅ)
|
| 413 |
+
memory = MemorySaver()
|
| 414 |
+
return graph.compile(checkpointer=memory)
|
| 415 |
+
else:
|
| 416 |
+
return graph.compile()
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ์์ด์ ํธ ์ธ์คํด์ค ์์ฑ (๋ชจ๋ ์ํฌํธ ์ ์๋ ์์ฑ)
|
| 420 |
+
agent = create_agent(enable_checkpointing=True)
|
CodeWeaver/src/agent/nodes.py
ADDED
|
@@ -0,0 +1,1212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeWeaver LangGraph ๋
ธ๋ ๊ตฌํ.
|
| 3 |
+
|
| 4 |
+
๊ฐ ๋
ธ๋๋ AgentState ๋๋ WorkerState๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๊ณ ์
๋ฐ์ดํธ๋ ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
|
| 5 |
+
๋ชจ๋ ๋
ธ๋๋ LangSmith๋ฅผ ํตํด ์๋์ผ๋ก ์ถ์ ๋ฉ๋๋ค.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
from typing import List, Literal, Optional, Union
|
| 12 |
+
|
| 13 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
| 14 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 15 |
+
from langgraph.graph import StateGraph, START, END
|
| 16 |
+
from langgraph.types import Send
|
| 17 |
+
|
| 18 |
+
from src.agent.state import AgentState, WorkerState, SearchResult
|
| 19 |
+
from src.agent.state import _MULTI_ANS_RESET_TOKEN
|
| 20 |
+
from src.tools.search_tools import (
|
| 21 |
+
search_github,
|
| 22 |
+
search_official_docs,
|
| 23 |
+
search_stackoverflow,
|
| 24 |
+
)
|
| 25 |
+
from src.utils.tracing import trace_node
|
| 26 |
+
from src.vector_db.qdrant_client import QdrantManager
|
| 27 |
+
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
# LLM ์ด๊ธฐํ (Gemini 2.5 Flash)
|
| 31 |
+
llm = ChatGoogleGenerativeAI(
|
| 32 |
+
model="gemini-2.5-flash-lite",
|
| 33 |
+
temperature=0.7,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Qdrant ๋งค๋์ ์ด๊ธฐํ
|
| 37 |
+
qdrant_manager = QdrantManager()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ==================== ๋ถ๋ชจ ๊ทธ๋ํ ๋
ธ๋ (AgentState ์ฌ์ฉ) ====================
|
| 41 |
+
|
| 42 |
+
@trace_node("create_plan")
|
| 43 |
+
def create_plan_node(state: AgentState) -> dict:
|
| 44 |
+
"""
|
| 45 |
+
์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ๊ณผ ๊ฐ์๋ฅผ ํ๋จํฉ๋๋ค.
|
| 46 |
+
|
| 47 |
+
Case:
|
| 48 |
+
- single_topic: ํ๋์ ์ฃผ์ (์๋ธ๊ทธ๋ํ 1ํ)
|
| 49 |
+
- multiple_questions: ๋
๋ฆฝ ์ง๋ฌธ 2๊ฐ (Send API๋ก ์๋ธ๊ทธ๋ํ 2ํ ๋ณ๋ ฌ)
|
| 50 |
+
- too_many: ๋
๋ฆฝ ์ง๋ฌธ 3๊ฐ ์ด์ (์๋ฌ ๋ฉ์์ง)
|
| 51 |
+
"""
|
| 52 |
+
user_question = state.user_question
|
| 53 |
+
logger.info("์ง๋ฌธ ๋ถ์ ๋ฐ ๊ณํ ์๋ฆฝ ์ค: %s", user_question[:50])
|
| 54 |
+
|
| 55 |
+
def _extract_question_candidates(text: str) -> List[str]:
|
| 56 |
+
"""์
๋ ฅ ๋ฌธ์์ด์์ '์ง๋ฌธ ํ๋ณด'๋ฅผ ์ต๋ํ ๋ณด์์ ์ผ๋ก ์ถ์ถํฉ๋๋ค(3๊ฐ ์ด์ ๊ฐ์ง์ฉ)."""
|
| 57 |
+
import re
|
| 58 |
+
|
| 59 |
+
if not text:
|
| 60 |
+
return []
|
| 61 |
+
|
| 62 |
+
t = text.strip()
|
| 63 |
+
# 1) ๋ฌผ์ํ ๊ธฐ๋ฐ ๋ถ๋ฆฌ
|
| 64 |
+
parts = re.split(r"[??]+", t)
|
| 65 |
+
candidates = [p.strip() for p in parts if p.strip()]
|
| 66 |
+
if len(candidates) >= 2 and re.search(r"[??]", t):
|
| 67 |
+
return candidates
|
| 68 |
+
|
| 69 |
+
# 2) ์ค๋ฐ๊ฟ/๋ฒํธ ๋งค๊ธฐ๊ธฐ ๊ธฐ๋ฐ
|
| 70 |
+
lines = [ln.strip() for ln in re.split(r"[\r\n]+", t) if ln.strip()]
|
| 71 |
+
numbered = []
|
| 72 |
+
for ln in lines:
|
| 73 |
+
if re.match(r"^\s*(\d+[\.\)]|[-*])\s+", ln):
|
| 74 |
+
numbered.append(re.sub(r"^\s*(\d+[\.\)]|[-*])\s+", "", ln).strip())
|
| 75 |
+
if len(numbered) >= 2:
|
| 76 |
+
return numbered
|
| 77 |
+
|
| 78 |
+
# 3) ๊ตฌ๋ถ์ ๊ธฐ๋ฐ(์ธ๋ฏธ์ฝ๋ก )
|
| 79 |
+
semi = [p.strip() for p in t.split(";") if p.strip()]
|
| 80 |
+
if len(semi) >= 2:
|
| 81 |
+
return semi
|
| 82 |
+
|
| 83 |
+
return [t]
|
| 84 |
+
|
| 85 |
+
def _hard_guard_too_many(text: str) -> Optional[dict]:
|
| 86 |
+
"""
|
| 87 |
+
ํ๋ ๊ฐ๋: ์ฌ์ฉ์๊ฐ '์ง๋ฌธ 3๊ฐ ์ด์'์ ํ ๋ฒ์ ๋์ง ๊ฒ์ผ๋ก ํ์คํ ๊ฒฝ์ฐ,
|
| 88 |
+
LLM ๋ถ๋ฅ์ ๋ฌด๊ดํ๊ฒ too_many๋ก ๊ฐ์ ํฉ๋๋ค.
|
| 89 |
+
"""
|
| 90 |
+
import re
|
| 91 |
+
|
| 92 |
+
if not text:
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
# ๊ฐ์ฅ ํ์คํ ๊ธฐ์ค: ๋ฌผ์ํ๊ฐ 3๊ฐ ์ด์
|
| 96 |
+
qmarks = len(re.findall(r"[??]", text))
|
| 97 |
+
if qmarks >= 3:
|
| 98 |
+
candidates = _extract_question_candidates(text)
|
| 99 |
+
msg = "์ฃ์กํฉ๋๋ค. ์ง๋ฌธ์ ํ ๋ฒ์ ์ต๋ 2๊ฐ๊น์ง ๊ฐ๋ฅํฉ๋๋ค. ๊ฐ์ฅ ์ค์ํ 2๊ฐ๋ง ๊ณจ๋ผ์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์."
|
| 100 |
+
return {
|
| 101 |
+
"case": "too_many",
|
| 102 |
+
"sub_questions": candidates,
|
| 103 |
+
"reasoning": f"๋ฌผ์ํ๊ฐ {qmarks}๊ฐ๋ก, 3๊ฐ ์ด์์ ๋
๋ฆฝ ์ง๋ฌธ์ผ๋ก ํ๋จํ์ต๋๋ค.",
|
| 104 |
+
"error_message": msg,
|
| 105 |
+
"steps_note": f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ ๊ฐ์ง(๋ฌผ์ํ {qmarks}๊ฐ) โ too_many๋ก ๊ฐ์ ",
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
# ๋ฒํธ ๋งค๊ธฐ๊ธฐ/๋ฆฌ์คํธ๋ก 3๊ฐ ์ด์
|
| 109 |
+
candidates = _extract_question_candidates(text)
|
| 110 |
+
if len(candidates) >= 3:
|
| 111 |
+
msg = "์ฃ์กํฉ๋๋ค. ์ง๋ฌธ์ ํ ๋ฒ์ ์ต๋ 2๊ฐ๊น์ง ๊ฐ๋ฅํฉ๋๋ค. ๊ฐ์ฅ ์ค์ํ 2๊ฐ๋ง ๊ณจ๋ผ์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์."
|
| 112 |
+
return {
|
| 113 |
+
"case": "too_many",
|
| 114 |
+
"sub_questions": candidates,
|
| 115 |
+
"reasoning": f"์ง๋ฌธ ํ๋ณด๊ฐ {len(candidates)}๊ฐ๋ก ๊ฐ์ง๋์ด 3๊ฐ ์ด์ ์ง๋ฌธ์ผ๋ก ํ๋จํ์ต๋๋ค.",
|
| 116 |
+
"error_message": msg,
|
| 117 |
+
"steps_note": f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ ๊ฐ์ง(ํ๋ณด {len(candidates)}๊ฐ) โ too_many๋ก ๊ฐ์ ",
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
# ํ๋ ๊ฐ๋(๊ฒฐ์ ๋ก ์ ) โ LLM์ด ์๋ชป ๋ถ๋ฅํ๋๋ผ๋ 3๊ฐ ์ด์์ด๋ฉด ๋ฌด์กฐ๊ฑด ์ฐจ๋จ
|
| 123 |
+
hard = _hard_guard_too_many(user_question)
|
| 124 |
+
if hard:
|
| 125 |
+
steps_delta = [
|
| 126 |
+
f"๐ ๊ณํ ํ์
: {hard['case']}",
|
| 127 |
+
f" ์๋ธ์ง๋ฌธ: {len(hard['sub_questions'])}๊ฐ",
|
| 128 |
+
f" ์ด์ : {hard['reasoning']}",
|
| 129 |
+
hard["steps_note"],
|
| 130 |
+
]
|
| 131 |
+
logger.info("๊ณํ ์๋ฆฝ ์๋ฃ(ํ๋ ๊ฐ๋): too_many, %d๊ฐ ์๋ธ์ง๏ฟฝ๏ฟฝ", len(hard["sub_questions"]))
|
| 132 |
+
return {
|
| 133 |
+
"plan": {
|
| 134 |
+
"case": hard["case"],
|
| 135 |
+
"sub_questions": hard["sub_questions"],
|
| 136 |
+
"reasoning": hard["reasoning"],
|
| 137 |
+
"error_message": hard["error_message"],
|
| 138 |
+
},
|
| 139 |
+
"is_multi_question": False,
|
| 140 |
+
"sub_question_index": 0,
|
| 141 |
+
"sub_question_text": None,
|
| 142 |
+
"original_multi_question": None,
|
| 143 |
+
"multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
|
| 144 |
+
"intermediate_steps": steps_delta,
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
plan_prompt = f"""์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ๊ณผ ๊ฐ์๋ฅผ ํ๋จํ์ธ์.
|
| 148 |
+
|
| 149 |
+
์ง๋ฌธ: {user_question}
|
| 150 |
+
|
| 151 |
+
**์ค์**: sub_questions์ ์ฉ๋๋ case์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค!
|
| 152 |
+
|
| 153 |
+
**Case 1: single_topic** (ํ๋์ ์ฃผ์ )
|
| 154 |
+
- ์: "Spring Security JWT ์ธ์ฆ ๊ตฌํ"
|
| 155 |
+
โ sub_questions: ["๊ฐ๋
", "๊ตฌํ", "์์ "]
|
| 156 |
+
โ ์ฉ๋: ๋ต๋ณ ์น์
๊ตฌ์กฐ (๊ฒ์์ ์๋ณธ ์ง๋ฌธ์ผ๋ก 1ํ๋ง)
|
| 157 |
+
โ ๊ฒ์: "Spring Security JWT ์ธ์ฆ ๊ตฌํ"
|
| 158 |
+
|
| 159 |
+
- ์: "React hooks ์๋ฒฝ ๊ฐ์ด๋"
|
| 160 |
+
โ sub_questions: ["hooks๋", "์ฃผ์ hooks", "์ค๋ฌด ํจํด"]
|
| 161 |
+
โ ์ฉ๋: ๋ต๋ณ ์น์
๊ตฌ์กฐ
|
| 162 |
+
โ ๊ฒ์: "React hooks ์๋ฒฝ ๊ฐ์ด๋"
|
| 163 |
+
|
| 164 |
+
**Case 2: multiple_questions** (์ฌ๋ฌ ๋
๋ฆฝ ์ง๋ฌธ, ์ต๋ 2๊ฐ)
|
| 165 |
+
- ์: "JWT๊ฐ ๋ญ์ผ? CORS๋?"
|
| 166 |
+
โ sub_questions: ["JWT๊ฐ ๋ญ์ผ?", "CORS๋?"]
|
| 167 |
+
โ ์ฉ๋: ๊ฐ ์ง๋ฌธ๋ง๋ค ๋ณ๋ ๊ฒ์
|
| 168 |
+
โ ๊ฒ์: "JWT๊ฐ ๋ญ์ผ?" (1ํ), "CORS๋?" (1ํ)
|
| 169 |
+
|
| 170 |
+
- ์: "Docker ์ฌ์ฉ๋ฒ์? Redis ์ค์น๋?"
|
| 171 |
+
โ sub_questions: ["Docker ์ฌ์ฉ๋ฒ์?", "Redis ์ค์น๋?"]
|
| 172 |
+
โ ์ฉ๋: ๊ฐ ์ง๋ฌธ๋ง๋ค ๋ณ๋ ๊ฒ์
|
| 173 |
+
|
| 174 |
+
**Case 3: too_many** (3๊ฐ ์ด์ ์ง๋ฌธ)
|
| 175 |
+
- ์: "JWT? CORS? Docker?"
|
| 176 |
+
โ ๋๋ฌด ๋ง์์ ์ฒ๋ฆฌ ๋ถ๊ฐ
|
| 177 |
+
โ error_message ์ ๊ณต
|
| 178 |
+
|
| 179 |
+
๊ท์น:
|
| 180 |
+
- single_topic: sub_questions๋ ์งง์ ํค์๋/๊ตฌ์ (1-5๊ฐ)
|
| 181 |
+
- multiple_questions: sub_questions๋ ์์ ํ ๋ฌธ์ฅ (์ ํํ 2๊ฐ๋ง)
|
| 182 |
+
- too_many: 3๊ฐ ์ด์์ด๋ฉด ์ด ์ผ์ด์ค๋ก ๋ถ๋ฅ
|
| 183 |
+
|
| 184 |
+
๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์:
|
| 185 |
+
{{
|
| 186 |
+
"case": "single_topic|multiple_questions|too_many",
|
| 187 |
+
"sub_questions": [...],
|
| 188 |
+
"reasoning": "์ด ์ผ์ด์ค๋ก ํ๋จํ ์ด์ ",
|
| 189 |
+
"error_message": "..." (too_many์ธ ๊ฒฝ์ฐ๋ง, ๊ทธ ์ธ๋ ๋น ๋ฌธ์์ด)
|
| 190 |
+
}}
|
| 191 |
+
|
| 192 |
+
JSON ์ธ์ ๋ค๋ฅธ ํ
์คํธ๋ ํฌํจํ์ง ๋ง์ธ์."""
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
import json
|
| 196 |
+
|
| 197 |
+
messages_to_llm = [HumanMessage(content=plan_prompt)]
|
| 198 |
+
response = llm.invoke(messages_to_llm)
|
| 199 |
+
|
| 200 |
+
# JSON ํ์ฑ
|
| 201 |
+
response_text = response.content.strip()
|
| 202 |
+
|
| 203 |
+
# JSON ๋ธ๋ก ์ถ์ถ
|
| 204 |
+
if "```json" in response_text:
|
| 205 |
+
response_text = response_text.split("```json")[1].split("```")[0].strip()
|
| 206 |
+
elif "```" in response_text:
|
| 207 |
+
response_text = response_text.split("```")[1].split("```")[0].strip()
|
| 208 |
+
|
| 209 |
+
plan_data = json.loads(response_text)
|
| 210 |
+
|
| 211 |
+
case = plan_data.get("case", "single_topic")
|
| 212 |
+
sub_questions = plan_data.get("sub_questions", [user_question])
|
| 213 |
+
reasoning = plan_data.get("reasoning", "")
|
| 214 |
+
error_message = plan_data.get("error_message", "")
|
| 215 |
+
|
| 216 |
+
# LLM ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ๋ค์๋ ํ ๋ฒ ๋ ํ๋ ๊ฐ๋ ์ ์ฉ (์์ ์ฅ์น)
|
| 217 |
+
hard2 = _hard_guard_too_many(user_question)
|
| 218 |
+
if hard2:
|
| 219 |
+
case = hard2["case"]
|
| 220 |
+
sub_questions = hard2["sub_questions"]
|
| 221 |
+
reasoning = hard2["reasoning"]
|
| 222 |
+
error_message = hard2["error_message"]
|
| 223 |
+
|
| 224 |
+
# ์ ํจ์ฑ ๊ฒ์ฆ
|
| 225 |
+
if not sub_questions or len(sub_questions) == 0:
|
| 226 |
+
sub_questions = [user_question]
|
| 227 |
+
case = "single_topic"
|
| 228 |
+
|
| 229 |
+
# multiple_questions์ผ ๋ 2๊ฐ ์ ํ ๊ฐ์
|
| 230 |
+
if case == "multiple_questions" and len(sub_questions) > 2:
|
| 231 |
+
sub_questions = sub_questions[:2]
|
| 232 |
+
reasoning += " (์ง๋ฌธ ์ ์ ํ: ์ต๋ 2๊ฐ)"
|
| 233 |
+
|
| 234 |
+
steps_delta = [
|
| 235 |
+
f"๐ ๊ณํ ํ์
: {case}",
|
| 236 |
+
f" ์๋ธ์ง๋ฌธ: {len(sub_questions)}๊ฐ",
|
| 237 |
+
f" ์ด์ : {reasoning}"
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
logger.info("๊ณํ ์๋ฆฝ ์๋ฃ: %s, %d๊ฐ ์๋ธ์ง๋ฌธ", case, len(sub_questions))
|
| 241 |
+
|
| 242 |
+
return {
|
| 243 |
+
"plan": {
|
| 244 |
+
"case": case,
|
| 245 |
+
"sub_questions": sub_questions,
|
| 246 |
+
"reasoning": reasoning,
|
| 247 |
+
"error_message": error_message
|
| 248 |
+
},
|
| 249 |
+
"is_multi_question": False,
|
| 250 |
+
"sub_question_index": 0,
|
| 251 |
+
"sub_question_text": None,
|
| 252 |
+
"original_multi_question": None,
|
| 253 |
+
"multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
|
| 254 |
+
"intermediate_steps": steps_delta
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.error("๊ณํ ์๋ฆฝ ์คํจ: %s", e, exc_info=True)
|
| 259 |
+
|
| 260 |
+
# ๊ธฐ๋ณธ๊ฐ: ์๋ณธ ์ง๋ฌธ ๊ทธ๋๋ก ์ฌ์ฉ
|
| 261 |
+
steps_delta = [
|
| 262 |
+
"โ ๏ธ ๊ณํ ์๋ฆฝ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: single_topic"
|
| 263 |
+
]
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"plan": {
|
| 267 |
+
"case": "single_topic",
|
| 268 |
+
"sub_questions": [user_question],
|
| 269 |
+
"reasoning": "๊ณํ ์๋ฆฝ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ",
|
| 270 |
+
"error_message": ""
|
| 271 |
+
},
|
| 272 |
+
"is_multi_question": False,
|
| 273 |
+
"sub_question_index": 0,
|
| 274 |
+
"sub_question_text": None,
|
| 275 |
+
"original_multi_question": None,
|
| 276 |
+
"multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
|
| 277 |
+
"intermediate_steps": steps_delta
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@trace_node("handle_too_many_questions")
|
| 282 |
+
def handle_too_many_questions_node(state: AgentState) -> dict:
|
| 283 |
+
"""3๊ฐ ์ด์ ์ง๋ฌธ ์ ์๋ด ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค."""
|
| 284 |
+
plan = state.plan or {}
|
| 285 |
+
error_message = plan.get("error_message", "")
|
| 286 |
+
sub_questions = plan.get("sub_questions", [])
|
| 287 |
+
|
| 288 |
+
logger.info("์ง๋ฌธ ์ ์ด๊ณผ: %d๊ฐ", len(sub_questions))
|
| 289 |
+
|
| 290 |
+
default_message = """์ฃ์กํฉ๋๋ค. ํ ๋ฒ์ ์ต๋ 2๊ฐ์ ์ง๋ฌธ๊น์ง๋ง ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
|
| 291 |
+
|
| 292 |
+
๋ค์ ์ค ํ๋๋ฅผ ์ ํํด์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์:
|
| 293 |
+
|
| 294 |
+
1. **ํ๋์ ์ฃผ์ ๋ก ํตํฉํด์ ์ง๋ฌธ**
|
| 295 |
+
์: "JWT ์ธ์ฆ๊ณผ CORS ์ค์ ์ ํจ๊ป ๊ตฌํํ๋ ๋ฐฉ๋ฒ"
|
| 296 |
+
|
| 297 |
+
2. **๊ฐ์ฅ ์ค์ํ 2๊ฐ ์ง๋ฌธ๋ง ์ ํ**
|
| 298 |
+
์: "JWT๊ฐ ๋ญ์ผ? ๋ด ์ฝ๋์ ์ด๋ป๊ฒ ์ ์ฉํด?"
|
| 299 |
+
|
| 300 |
+
3. **์ง๋ฌธ์ ๋๋ ์ ์์ฐจ์ ์ผ๋ก ์ง๋ฌธ**
|
| 301 |
+
์: ๋จผ์ "JWT๊ฐ ๋ญ์ผ?" ์ง๋ฌธ โ ๋ต๋ณ ํ์ธ โ ๋ค์ ์ง๋ฌธ
|
| 302 |
+
|
| 303 |
+
์ด๋ป๊ฒ ๋์๋๋ฆด๊น์?"""
|
| 304 |
+
|
| 305 |
+
final_message = error_message if error_message else default_message
|
| 306 |
+
|
| 307 |
+
steps_delta = [
|
| 308 |
+
f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ: {len(sub_questions)}๊ฐ",
|
| 309 |
+
"๐ฌ ์๋ด ๋ฉ์์ง ์ ๊ณต (๋ํ ๊ณ์ ๊ฐ๋ฅ)"
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
return {
|
| 313 |
+
"final_answer": final_message,
|
| 314 |
+
"intermediate_steps": steps_delta
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@trace_node("combine_answers")
|
| 319 |
+
def combine_answers_node(state: AgentState) -> dict:
|
| 320 |
+
"""
|
| 321 |
+
Fan-in: ๋ชจ๋ Send๊ฐ ์๋ฃ๋๋ฉด multi_answers๋ฅผ ์กฐํฉํฉ๋๋ค.
|
| 322 |
+
"""
|
| 323 |
+
answers = state.multi_answers
|
| 324 |
+
original_question = state.original_multi_question or state.user_question
|
| 325 |
+
|
| 326 |
+
if not answers:
|
| 327 |
+
logger.error("๋ค์ค ๋ต๋ณ์ด ๋น์ด์์")
|
| 328 |
+
return {
|
| 329 |
+
"final_answer": "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.",
|
| 330 |
+
"intermediate_steps": ["โ multi_answers ๋น์ด์์"]
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
# ์ธ๋ฑ์ค ์์ผ๋ก ์ ๋ ฌ
|
| 334 |
+
answers.sort(key=lambda x: x["index"])
|
| 335 |
+
|
| 336 |
+
# Markdown ํ์์ผ๋ก ์กฐํฉ
|
| 337 |
+
combined_parts = []
|
| 338 |
+
for ans in answers:
|
| 339 |
+
section = f"""## {ans['index']+1}. {ans['question']}
|
| 340 |
+
|
| 341 |
+
{ans['answer']}"""
|
| 342 |
+
combined_parts.append(section)
|
| 343 |
+
|
| 344 |
+
combined = "\n\n---\n\n".join(combined_parts)
|
| 345 |
+
|
| 346 |
+
# ํค๋ ์ถ๊ฐ
|
| 347 |
+
header = f"# ๋ค์ค ์ง๋ฌธ ๋ต๋ณ\n\n์๋ณธ ์ง๋ฌธ: {original_question}\n\n---\n\n"
|
| 348 |
+
final_combined = header + combined
|
| 349 |
+
|
| 350 |
+
logger.info("๋ค์ค ๋ต๋ณ ์กฐํฉ ์๋ฃ: %d๊ฐ", len(answers))
|
| 351 |
+
|
| 352 |
+
return {
|
| 353 |
+
"final_answer": final_combined,
|
| 354 |
+
"intermediate_steps": [f"โ
{len(answers)}๊ฐ ๋ต๋ณ ์กฐํฉ ์๋ฃ"]
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ==================== ์๋ธ๊ทธ๋ํ ๋
ธ๋ (WorkerState ์ฌ์ฉ) ====================
|
| 359 |
+
|
| 360 |
+
@trace_node("analyze_question")
|
| 361 |
+
async def analyze_question_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 362 |
+
"""
|
| 363 |
+
์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ์ ๋ถ๋ฅํ๊ณ ์บ์ ์ ๊ฒฉ์ฑ์ ํ๋จํฉ๋๋ค.
|
| 364 |
+
|
| 365 |
+
๐ง FIX: ๋ค์ค ์ง๋ฌธ ๋ชจ๋์ผ ๋๋ messages๋ฅผ ๋ฌด์ํ๊ณ ๋
๋ฆฝ ์ง๋ฌธ์ผ๋ก๋ง ๋ถ์
|
| 366 |
+
"""
|
| 367 |
+
# ๐ง [FIX] WorkerState์ผ ๊ฒฝ์ฐ processing_question ์ฌ์ฉ
|
| 368 |
+
if isinstance(state, WorkerState):
|
| 369 |
+
user_question = state.processing_question
|
| 370 |
+
# ๐ง [FIX] ์ด๋ฆ ๋ณ๊ฒฝ๋ ํ๋ ์ฌ์ฉ
|
| 371 |
+
is_multi = state.worker_is_multi
|
| 372 |
+
else:
|
| 373 |
+
user_question = state.user_question
|
| 374 |
+
is_multi = getattr(state, 'is_multi_question', False)
|
| 375 |
+
|
| 376 |
+
messages = state.messages
|
| 377 |
+
|
| 378 |
+
# ๋ํ ๋งฅ๋ฝ ๊ตฌ์ฑ (๋ค์ค ์ง๋ฌธ ๋ชจ๋๊ฐ ์๋ ๋๋ง)
|
| 379 |
+
has_history = messages and len(messages) > 1 and not is_multi
|
| 380 |
+
context_info = ""
|
| 381 |
+
|
| 382 |
+
if has_history:
|
| 383 |
+
context_info = "\n์ด์ ๋ํ ๋งฅ๋ฝ:\n"
|
| 384 |
+
for msg in messages[-4:-1]:
|
| 385 |
+
if hasattr(msg, 'type') and hasattr(msg, 'content'):
|
| 386 |
+
role = "์ฌ์ฉ์" if msg.type == "human" else "AI"
|
| 387 |
+
context_info += f"{role}: {msg.content[:100]}\n"
|
| 388 |
+
|
| 389 |
+
# ๐ง ๋ค์ค ์ง๋ฌธ ๋ชจ๋ ๊ฐ์ ์ฒ๋ฆฌ
|
| 390 |
+
if is_multi:
|
| 391 |
+
context_info = "\nโ ๏ธ ์ฃผ์: ์ด ์ง๋ฌธ์ ๋ค์ค ์ง๋ฌธ์ ์ผ๋ถ์
๋๋ค. ๋
๋ฆฝ์ ์ธ ์ง๋ฌธ์ผ๋ก๋ง ํ๋จํ์ธ์.\n"
|
| 392 |
+
|
| 393 |
+
analysis_prompt = f"""์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ์ ๋ถ๋ฅํ๊ณ , ์บ์ ์ ๊ฒฉ์ฑ์ ํ๋จํ์ธ์.
|
| 394 |
+
|
| 395 |
+
{context_info}
|
| 396 |
+
ํ์ฌ ์ง๋ฌธ: {user_question}
|
| 397 |
+
|
| 398 |
+
๋ถ๋ฅ ๊ธฐ์ค:
|
| 399 |
+
|
| 400 |
+
1. **clarification** (๋ณด์ถฉ/ํ์ ๋ณ๊ฒฝ ์์ฒญ)
|
| 401 |
+
- ์ด์ ๋ต๋ณ/๋ํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก "์ค๋ช
๋ฐฉ์"์ ๋ฐ๊พธ๊ฑฐ๋ ๋ณด์ถฉ์ ์์ฒญ
|
| 402 |
+
- ์: "์ข ๋ ์ฝ๊ฒ ์ค๋ช
ํด์ค", "์์ ์ฝ๋๋ก ๋ณด์ฌ์ค", "ํ ์ค๋ก ์์ฝํด์ค"
|
| 403 |
+
- should_cache = false, canonical_question = null
|
| 404 |
+
|
| 405 |
+
2. **new_topic** (๋ํ ์ค ์ ๊ฐ๋
์ง๋ฌธ)
|
| 406 |
+
- ๋ํ๊ฐ ์ด์ด์ง๋ ์ค์ด์ง๋ง, ์ง๋ฌธ ์์ฒด๊ฐ ๋
๋ฆฝ์ ์ผ๋ก ์ฑ๋ฆฝํ๋ '์ ๊ฐ๋
/์ ์/๋น๊ต/์ฌ์ฉ๋ฒ' ์ง๋ฌธ
|
| 407 |
+
- ์: "Event Listener๋ ๋ญ์ผ?", "CORS๊ฐ ๋ญ์ผ?"
|
| 408 |
+
- should_cache = true, canonical_question ์์ฑ
|
| 409 |
+
|
| 410 |
+
3. **independent** (์์ ๋
๋ฆฝ ์ง๋ฌธ)
|
| 411 |
+
- ์ด์ ๋ํ ์์ด๋ ์ดํด ๊ฐ๋ฅํ ์ผ๋ฐ ์ง๋ฌธ
|
| 412 |
+
- ์: "Spring Security๊ฐ ๋ญ์ผ?", "Docker Compose ์ฌ์ฉ๋ฒ์?"
|
| 413 |
+
- should_cache = true, canonical_question ์์ฑ
|
| 414 |
+
|
| 415 |
+
๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์:
|
| 416 |
+
{{
|
| 417 |
+
"question_type": "clarification|new_topic|independent",
|
| 418 |
+
"should_cache": true|false,
|
| 419 |
+
"reasoning": "๋ถ๋ฅ ์ด์ 1-2๋ฌธ์ฅ",
|
| 420 |
+
"canonical_question": "์บ์ํ ์ ๊ทํ๋ ์ง๋ฌธ (should_cache๊ฐ true์ธ ๊ฒฝ์ฐ์๋ง, ์๋๋ฉด null)"
|
| 421 |
+
}}
|
| 422 |
+
|
| 423 |
+
JSON ์ธ์ ๋ค๋ฅธ ํ
์คํธ๋ ํฌํจํ์ง ๋ง์ธ์."""
|
| 424 |
+
|
| 425 |
+
try:
|
| 426 |
+
messages_to_llm = [HumanMessage(content=analysis_prompt)]
|
| 427 |
+
response = llm.invoke(messages_to_llm)
|
| 428 |
+
|
| 429 |
+
import json
|
| 430 |
+
response_text = response.content.strip()
|
| 431 |
+
|
| 432 |
+
if "```json" in response_text:
|
| 433 |
+
response_text = response_text.split("```json")[1].split("```")[0].strip()
|
| 434 |
+
elif "```" in response_text:
|
| 435 |
+
response_text = response_text.split("```")[1].split("```")[0].strip()
|
| 436 |
+
|
| 437 |
+
analysis = json.loads(response_text)
|
| 438 |
+
|
| 439 |
+
question_type = analysis.get("question_type", "independent")
|
| 440 |
+
should_cache = analysis.get("should_cache", False)
|
| 441 |
+
reasoning = analysis.get("reasoning", "")
|
| 442 |
+
canonical_question = analysis.get("canonical_question", user_question)
|
| 443 |
+
|
| 444 |
+
# ์ ํจ์ฑ ๊ฒ์ฆ
|
| 445 |
+
if question_type not in ["clarification", "new_topic", "independent"]:
|
| 446 |
+
question_type = "independent"
|
| 447 |
+
|
| 448 |
+
# ๐ง CRITICAL: ๋ค์ค ์ง๋ฌธ ๋ชจ๋์ผ ๋๋ ๋ฌด์กฐ๊ฑด independent๋ก ๊ฐ์
|
| 449 |
+
if is_multi and question_type == "clarification":
|
| 450 |
+
logger.warning("๋ค์ค ์ง๋ฌธ ๋ชจ๋์์ clarification ๊ฐ์ง โ independent๋ก ๊ฐ์ ๋ณ๊ฒฝ")
|
| 451 |
+
question_type = "independent"
|
| 452 |
+
should_cache = True
|
| 453 |
+
reasoning = "๋ค์ค ์ง๋ฌธ ๋ชจ๋: ๋
๋ฆฝ ์ง๋ฌธ์ผ๋ก ๊ฐ์ ๋ถ๋ฅ"
|
| 454 |
+
|
| 455 |
+
# ์ ์ฑ
๋ณด์
|
| 456 |
+
if question_type == "clarification":
|
| 457 |
+
should_cache = False
|
| 458 |
+
canonical_question = None
|
| 459 |
+
else:
|
| 460 |
+
if canonical_question is None or (isinstance(canonical_question, str) and not canonical_question.strip()):
|
| 461 |
+
canonical_question = user_question
|
| 462 |
+
|
| 463 |
+
steps_delta = [
|
| 464 |
+
"__RESET_STEPS__",
|
| 465 |
+
f"๐ ์ง๋ฌธ ๋ถ์: {question_type} (์บ์ ์ฌ๋ถ: {should_cache})",
|
| 466 |
+
]
|
| 467 |
+
|
| 468 |
+
return {
|
| 469 |
+
"question_type": question_type,
|
| 470 |
+
"should_cache": should_cache,
|
| 471 |
+
"analysis_reasoning": reasoning,
|
| 472 |
+
"canonical_question": canonical_question if should_cache else None,
|
| 473 |
+
"intermediate_steps": steps_delta
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
except Exception as e:
|
| 477 |
+
logger.error("์ง๋ฌธ ๋ถ์ ์คํจ: %s", e, exc_info=True)
|
| 478 |
+
|
| 479 |
+
steps_delta = [
|
| 480 |
+
"__RESET_STEPS__",
|
| 481 |
+
"โ ๏ธ ์ง๋ฌธ ๋ถ์ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: independent",
|
| 482 |
+
]
|
| 483 |
+
|
| 484 |
+
return {
|
| 485 |
+
"question_type": "independent",
|
| 486 |
+
"should_cache": True,
|
| 487 |
+
"analysis_reasoning": "๋ถ์ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ",
|
| 488 |
+
"canonical_question": user_question,
|
| 489 |
+
"intermediate_steps": steps_delta
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
@trace_node("check_cache")
|
| 494 |
+
async def check_cache_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 495 |
+
"""๋ฒกํฐ DB ์บ์์์ ์ ์ฌํ ์ง๋ฌธ์ ๊ฒ์ํฉ๋๋ค."""
|
| 496 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 497 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 498 |
+
question_for_lookup = state.canonical_question or current_q
|
| 499 |
+
logger.info("์บ์ ํ์ธ ์ค: %s", question_for_lookup[:50])
|
| 500 |
+
|
| 501 |
+
try:
|
| 502 |
+
cached_result = await qdrant_manager.search_cache(
|
| 503 |
+
question=question_for_lookup,
|
| 504 |
+
threshold=0.85
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
updates = {}
|
| 508 |
+
steps_delta: List[str] = []
|
| 509 |
+
|
| 510 |
+
if cached_result:
|
| 511 |
+
updates["cached_result"] = cached_result
|
| 512 |
+
steps_delta.append(f"โ
์บ์ ํํธ (๋ต๋ณ ๊ธธ์ด: {len(cached_result)}์)")
|
| 513 |
+
logger.info("์บ์ ํํธ")
|
| 514 |
+
else:
|
| 515 |
+
updates["cached_result"] = None
|
| 516 |
+
steps_delta.append("โ ์บ์ ๋ฏธ์ค: ์๋ก์ด ๊ฒ์ ํ์")
|
| 517 |
+
logger.info("์บ์ ๋ฏธ์ค")
|
| 518 |
+
|
| 519 |
+
except Exception as e:
|
| 520 |
+
logger.error("์บ์ ํ์ธ ์คํจ: %s", e, exc_info=True)
|
| 521 |
+
updates["cached_result"] = None
|
| 522 |
+
steps_delta.append(f"โ ๏ธ ์บ์ ํ์ธ ์ค๋ฅ: {str(e)}")
|
| 523 |
+
|
| 524 |
+
updates["intermediate_steps"] = steps_delta
|
| 525 |
+
return updates
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
@trace_node("return_cached_answer")
|
| 529 |
+
def return_cached_answer_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 530 |
+
"""์บ์ ํํธ ์ ์ ์ฅ๋ ๋ต๋ณ์ ๋ฐํํฉ๋๋ค."""
|
| 531 |
+
logger.info("์บ์๋ ๋ต๋ณ ๋ฐํ")
|
| 532 |
+
|
| 533 |
+
cached_answer = state.cached_result
|
| 534 |
+
is_multi = isinstance(state, WorkerState) and state.worker_is_multi
|
| 535 |
+
|
| 536 |
+
if is_multi:
|
| 537 |
+
return {
|
| 538 |
+
"multi_answers": [{
|
| 539 |
+
"index": state.worker_idx,
|
| 540 |
+
"question": state.worker_sub_text or state.processing_question,
|
| 541 |
+
"answer": cached_answer
|
| 542 |
+
}]
|
| 543 |
+
}
|
| 544 |
+
else:
|
| 545 |
+
# ๐ง [FIX] messages์ AIMessage ์ถ๊ฐํ์ฌ ํ์คํ ๋ฆฌ ์ ์ฅ ๋ณด์ฅ
|
| 546 |
+
steps_delta = ["๐พ ์บ์๋ ๋ต๋ณ ๋ฐํ (๊ฒ์ ์๋ต)"]
|
| 547 |
+
return {
|
| 548 |
+
"final_answer": cached_answer,
|
| 549 |
+
"messages": [AIMessage(content=cached_answer)], # ๐ ํต์ฌ ์์
|
| 550 |
+
"intermediate_steps": steps_delta
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
@trace_node("generate_with_history")
|
| 555 |
+
async def generate_with_history_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 556 |
+
"""
|
| 557 |
+
๋ํ ํ์คํ ๋ฆฌ๋ง ์ฌ์ฉํ์ฌ ํ์ ์ง๋ฌธ์ ๋ต๋ณํฉ๋๋ค.
|
| 558 |
+
|
| 559 |
+
์์ ์ฌํญ:
|
| 560 |
+
1. ๋ฌธ๋งฅ ์ค์ผ ๋ฐฉ์ง: ๋ฐ๋ก ์ง์ ์ ๋ํ(์ง๋ฌธ+๋ต๋ณ)๋ง ์ฐธ์กฐํ๋๋ก ์ฌ๋ผ์ด์ฑ ์ ์ฉ
|
| 561 |
+
2. ํ์คํ ๋ฆฌ ์ ์ฅ: AIMessage ๋ฐํ ์ถ๊ฐ (๋ํ ๋๊น ๋ฐฉ์ง)
|
| 562 |
+
"""
|
| 563 |
+
# 1. ํ์ฌ ์ง๋ฌธ ์ถ์ถ
|
| 564 |
+
user_question = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 565 |
+
messages_history = state.messages
|
| 566 |
+
|
| 567 |
+
logger.info("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ: %s", user_question[:50])
|
| 568 |
+
|
| 569 |
+
# 2. ๋ํ ๋งฅ๋ฝ ๊ตฌ์ฑ (Context Pollution ๋ฐฉ์ง)
|
| 570 |
+
context_prompt = "์ด์ ๋ํ๋ฅผ ์ฐธ๊ณ ํ์ฌ ํ์ ์ง๋ฌธ์ ๋ต๋ณํ์ธ์.\n\n"
|
| 571 |
+
|
| 572 |
+
# [ํต์ฌ] ํ์ฌ ์ง๋ฌธ์ ์ ์ธํ ๊ณผ๊ฑฐ ๊ธฐ๋ก ์ค '๊ฐ์ฅ ์ต๊ทผ 2๊ฐ(์ง์ ์ง๋ฌธ+๋ต๋ณ)'๋ง ์ฐธ์กฐ
|
| 573 |
+
prev_messages = messages_history[:-1] if messages_history else []
|
| 574 |
+
recent_context = prev_messages[-2:] if prev_messages else []
|
| 575 |
+
|
| 576 |
+
if recent_context:
|
| 577 |
+
context_prompt += "์ง์ ๋ํ ๋ด์ญ:\n"
|
| 578 |
+
for msg in recent_context:
|
| 579 |
+
if hasattr(msg, 'type') and hasattr(msg, 'content'):
|
| 580 |
+
role = "์ฌ์ฉ์" if msg.type == "human" else "AI"
|
| 581 |
+
context_prompt += f"{role}: {msg.content}\n\n"
|
| 582 |
+
|
| 583 |
+
context_prompt += f"ํ์ฌ ์ง๋ฌธ: {user_question}\n\n"
|
| 584 |
+
context_prompt += "์์ '์ง์ ๋ํ ๋ด์ญ'์๋ง ์ง์คํ์ฌ ๋ต๋ณํ์ธ์. ๊ทธ ์ธ์ ์ด์ ์ฃผ์ ๋ ๋ถํ์ํ ๋งฅ๋ฝ์ ์ธ๊ธํ์ง ๋ง์ธ์."
|
| 585 |
+
|
| 586 |
+
updates = {}
|
| 587 |
+
steps_delta: List[str] = []
|
| 588 |
+
|
| 589 |
+
try:
|
| 590 |
+
# 3. LLM ํธ์ถ
|
| 591 |
+
response = llm.invoke([HumanMessage(content=context_prompt)])
|
| 592 |
+
final_answer = response.content.strip()
|
| 593 |
+
|
| 594 |
+
# 4. ์ํ ์
๋ฐ์ดํธ
|
| 595 |
+
is_multi = isinstance(state, WorkerState) and state.worker_is_multi
|
| 596 |
+
|
| 597 |
+
if is_multi:
|
| 598 |
+
# ๋ค์ค ์ง๋ฌธ ๋ชจ๋ (์์ธ์ ์ํฉ)
|
| 599 |
+
return {
|
| 600 |
+
"multi_answers": [{
|
| 601 |
+
"index": state.worker_idx,
|
| 602 |
+
"question": state.worker_sub_text or user_question,
|
| 603 |
+
"answer": final_answer
|
| 604 |
+
}]
|
| 605 |
+
}
|
| 606 |
+
else:
|
| 607 |
+
# ๋จ์ผ ์ง๋ฌธ ๋ชจ๋ (์ ์ ์ผ์ด์ค)
|
| 608 |
+
updates["final_answer"] = final_answer
|
| 609 |
+
# [ํต์ฌ] ๋ํ ํ์คํ ๋ฆฌ์ AI ๋ต๋ณ์ ์ถ๊ฐํ์ฌ ๋ค์ ํด์์ ์ฐธ์กฐ ๊ฐ๋ฅํ๊ฒ ํจ
|
| 610 |
+
updates["messages"] = [AIMessage(content=final_answer)]
|
| 611 |
+
|
| 612 |
+
steps_delta.append(f"๐ฌ ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ (๊ธธ์ด: {len(final_answer)}์)")
|
| 613 |
+
steps_delta.append("โ ๏ธ ์บ์ ์ ์ฅ ์๋ต (๋ณด์ถฉ ์์ฒญ)")
|
| 614 |
+
|
| 615 |
+
logger.info("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ ์๋ฃ")
|
| 616 |
+
|
| 617 |
+
except Exception as e:
|
| 618 |
+
logger.error("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ ์คํจ: %s", e, exc_info=True)
|
| 619 |
+
|
| 620 |
+
if is_multi:
|
| 621 |
+
return {
|
| 622 |
+
"multi_answers": [{
|
| 623 |
+
"index": state.worker_idx,
|
| 624 |
+
"question": state.worker_sub_text or user_question,
|
| 625 |
+
"answer": "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์."
|
| 626 |
+
}]
|
| 627 |
+
}
|
| 628 |
+
else:
|
| 629 |
+
updates["final_answer"] = "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์."
|
| 630 |
+
steps_delta.append(f"โ ๋ต๋ณ ์์ฑ ์คํจ: {str(e)}")
|
| 631 |
+
|
| 632 |
+
updates["intermediate_steps"] = steps_delta
|
| 633 |
+
return updates
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
@trace_node("classify_intent")
|
| 637 |
+
def classify_intent_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 638 |
+
"""
|
| 639 |
+
LLM์ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ง๋ฌธ์ ์๋๋ฅผ ๋ถ๋ฅํฉ๋๋ค.
|
| 640 |
+
|
| 641 |
+
๐ง CRITICAL:
|
| 642 |
+
- refined_question์ด ์์ผ๋ฉด ๊ทธ๊ฒ์ ์ฌ์ฉ, ์์ผ๋ฉด user_question ์ฌ์ฉ
|
| 643 |
+
- WorkerState ํ๋๋ง ๋ฐํ (๋ถ๋ชจ AgentState์ ์ถฉ๋ ๋ฐฉ์ง)
|
| 644 |
+
- โ ์ ๋ ๋ฐํํ๋ฉด ์ ๋๋ ๊ฒ๋ค: user_question, messages
|
| 645 |
+
"""
|
| 646 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 647 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 648 |
+
question_to_classify = state.refined_question if hasattr(state, 'refined_question') and state.refined_question else current_q
|
| 649 |
+
|
| 650 |
+
logger.info("์๋ ๋ถ๋ฅ ์ค: %s", question_to_classify[:50])
|
| 651 |
+
|
| 652 |
+
classification_prompt = f"""์ง๋ฌธ์ ๋ค์ ์ธ ๊ฐ์ง ์๋ ์ค ํ๋๋ก ๋ถ๋ฅํ์ธ์:
|
| 653 |
+
|
| 654 |
+
1. debugging: ์๋ฌ ํด๊ฒฐ, ๋ฒ๊ทธ ์์ , ๋ฌธ์ ํด๊ฒฐ
|
| 655 |
+
2. learning: ๊ฐ๋
ํ์ต, ์๋ฆฌ ์ดํด, ํํ ๋ฆฌ์ผ
|
| 656 |
+
3. code_review: ์ฝ๋ ๊ฐ์ , ๋ฆฌํฉํ ๋ง, ๋ฒ ์คํธ ํ๋ํฐ์ค
|
| 657 |
+
|
| 658 |
+
์ง๋ฌธ: {question_to_classify}
|
| 659 |
+
|
| 660 |
+
๋ฐ๋์ debugging, learning, code_review ์ค ํ๋๋ง ๋ตํ์ธ์."""
|
| 661 |
+
|
| 662 |
+
updates = {}
|
| 663 |
+
steps_delta: List[str] = []
|
| 664 |
+
|
| 665 |
+
try:
|
| 666 |
+
messages = [
|
| 667 |
+
SystemMessage(content="๋น์ ์ ๊ฐ๋ฐ์ ์ง๋ฌธ์ ๋ถ๋ฅํ๋ ์ ๋ฌธ๊ฐ์
๋๋ค."),
|
| 668 |
+
HumanMessage(content=classification_prompt)
|
| 669 |
+
]
|
| 670 |
+
|
| 671 |
+
response = llm.invoke(messages)
|
| 672 |
+
intent_raw = response.content.strip().lower()
|
| 673 |
+
|
| 674 |
+
# ์ ํจํ ์๋๋ก ์ ๊ทํ
|
| 675 |
+
valid_intents = ["debugging", "learning", "code_review"]
|
| 676 |
+
intent = next((i for i in valid_intents if i in intent_raw), "learning")
|
| 677 |
+
|
| 678 |
+
updates["detected_intent"] = intent
|
| 679 |
+
steps_delta.append(f"๐ฏ ์๋ ๋ถ๋ฅ: {intent}")
|
| 680 |
+
logger.info("์๋ ๋ถ๋ฅ ์๋ฃ: %s", intent)
|
| 681 |
+
|
| 682 |
+
except Exception as e:
|
| 683 |
+
logger.error("์๋ ๋ถ๋ฅ ์คํจ: %s", e, exc_info=True)
|
| 684 |
+
updates["detected_intent"] = "learning"
|
| 685 |
+
steps_delta.append("โ ๏ธ ์๋ ๋ถ๋ฅ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: learning")
|
| 686 |
+
|
| 687 |
+
updates["intermediate_steps"] = steps_delta
|
| 688 |
+
|
| 689 |
+
# ๐ง CRITICAL: WorkerState ํ๋๋ง ๋ฐํ
|
| 690 |
+
# โ
OK: detected_intent, intermediate_steps
|
| 691 |
+
# โ ์ ๋ ๋ฐํํ๋ฉด ์ ๋จ: user_question, messages
|
| 692 |
+
return updates
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
@trace_node("search_stackoverflow")
|
| 696 |
+
def search_stackoverflow_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 697 |
+
"""Stack Overflow์์ ๊ฒ์์ ์ํํฉ๋๋ค."""
|
| 698 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 699 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 700 |
+
question_to_use = state.refined_question if hasattr(state, 'refined_question') and state.refined_question else current_q
|
| 701 |
+
|
| 702 |
+
intent = state.detected_intent or "learning"
|
| 703 |
+
count = 5 if intent == "debugging" else 3
|
| 704 |
+
|
| 705 |
+
logger.info("Stack Overflow ๊ฒ์ ์์: %d๊ฐ", count)
|
| 706 |
+
|
| 707 |
+
try:
|
| 708 |
+
results = search_stackoverflow(question_to_use, count)
|
| 709 |
+
logger.info("Stack Overflow์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results))
|
| 710 |
+
|
| 711 |
+
# ๐ง FIX: intermediate_steps ์ ๊ฑฐ
|
| 712 |
+
return {
|
| 713 |
+
"search_results": results,
|
| 714 |
+
# intermediate_steps ์ ๊ฑฐ! (๋ณ๋ ฌ ์ถฉ๋ ๋ฐฉ์ง)
|
| 715 |
+
}
|
| 716 |
+
except Exception as e:
|
| 717 |
+
logger.error("Stack Overflow ๊ฒ์ ์คํจ: %s", e)
|
| 718 |
+
return {}
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
@trace_node("search_github")
|
| 722 |
+
def search_github_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 723 |
+
"""GitHub Issues/Discussions์์ ๊ฒ์์ ์ํํฉ๋๋ค."""
|
| 724 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 725 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 726 |
+
question_to_use = state.refined_question if hasattr(state, 'refined_question') and state.refined_question else current_q
|
| 727 |
+
|
| 728 |
+
intent = state.detected_intent or "learning"
|
| 729 |
+
count = 5 if intent == "code_review" else 3 if intent == "learning" else 2
|
| 730 |
+
|
| 731 |
+
logger.info("GitHub ๊ฒ์ ์์: %d๊ฐ", count)
|
| 732 |
+
|
| 733 |
+
try:
|
| 734 |
+
results = search_github(question_to_use, count)
|
| 735 |
+
logger.info("GitHub์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results))
|
| 736 |
+
|
| 737 |
+
# ๐ง FIX: intermediate_steps ์ ๊ฑฐ
|
| 738 |
+
return {
|
| 739 |
+
"search_results": results,
|
| 740 |
+
# intermediate_steps ์ ๊ฑฐ! (๋ณ๋ ฌ ์ถฉ๋ ๋ฐฉ์ง)
|
| 741 |
+
}
|
| 742 |
+
except Exception as e:
|
| 743 |
+
logger.error("GitHub ๊ฒ์ ์คํจ: %s", e)
|
| 744 |
+
return {}
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
@trace_node("search_official_docs")
|
| 748 |
+
def search_official_docs_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 749 |
+
"""๊ณต์ ๋ฌธ์/Tavily์์ ๊ฒ์์ ์ํํฉ๋๋ค."""
|
| 750 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 751 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 752 |
+
question_to_use = state.refined_question if hasattr(state, 'refined_question') and state.refined_question else current_q
|
| 753 |
+
|
| 754 |
+
intent = state.detected_intent or "learning"
|
| 755 |
+
count = 5 if intent == "learning" else 2
|
| 756 |
+
|
| 757 |
+
logger.info("๊ณต์ ๋ฌธ์ ๊ฒ์ ์์: %d๊ฐ", count)
|
| 758 |
+
|
| 759 |
+
try:
|
| 760 |
+
results = search_official_docs(question_to_use, count)
|
| 761 |
+
logger.info("๊ณต์ ๋ฌธ์์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results))
|
| 762 |
+
|
| 763 |
+
# ๐ง FIX: intermediate_steps ์ ๊ฑฐ
|
| 764 |
+
return {
|
| 765 |
+
"search_results": results,
|
| 766 |
+
# intermediate_steps ์ ๊ฑฐ! (๋ณ๋ ฌ ์ถฉ๋ ๋ฐฉ์ง)
|
| 767 |
+
}
|
| 768 |
+
except Exception as e:
|
| 769 |
+
logger.error("๊ณต์ ๋ฌธ์ ๊ฒ์ ์คํจ: %s", e)
|
| 770 |
+
return {}
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
@trace_node("collect_results")
|
| 774 |
+
def collect_results_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 775 |
+
"""๋ณ๋ ฌ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์์งํ๊ณ ์นด์ดํธํฉ๋๋ค."""
|
| 776 |
+
total_results = len(state.search_results)
|
| 777 |
+
|
| 778 |
+
logger.info("๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: %d๊ฐ", total_results)
|
| 779 |
+
|
| 780 |
+
# ๐ง FIX: ๋ก๊ทธ๋ง ์ฐ๊ณ , intermediate_steps๋ ์
๋ฐ์ดํธํ์ง ์์
|
| 781 |
+
# (๋ณ๋ ฌ ๋
ธ๋์์ intermediate_steps ์
๋ฐ์ดํธ ์ ์ถฉ๋ ๋ฐ์)
|
| 782 |
+
|
| 783 |
+
return {} # ๋น ๋์
๋๋ฆฌ ๋ฐํ (์ํ ๋ณ๊ฒฝ ์์)
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
@trace_node("evaluate_results")
|
| 787 |
+
def evaluate_results_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 788 |
+
"""๊ฒ์ ๊ฒฐ๊ณผ์ ๊ฐ์์ ํ์ง์ ๋ชจ๋ ํ๊ฐํฉ๋๋ค."""
|
| 789 |
+
search_results = state.search_results
|
| 790 |
+
refinement_count = state.refinement_count
|
| 791 |
+
|
| 792 |
+
result_count = len(search_results)
|
| 793 |
+
|
| 794 |
+
logger.info("๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: %d๊ฐ (๊ฐ์ ํ์: %d)", result_count, refinement_count)
|
| 795 |
+
|
| 796 |
+
# ์์ ์ฅ์น: ์ด๋ฏธ 1ํ ๊ฐ์ ํ์ผ๋ฉด ๋ ์ด์ ๊ฐ์ ํ์ง ์์
|
| 797 |
+
if refinement_count >= 1:
|
| 798 |
+
steps_delta = [
|
| 799 |
+
f"โ ๏ธ ์ต๋ ๊ฐ์ ํ์ ๋๋ฌ ({refinement_count}ํ), ํ์ฌ ๊ฒฐ๊ณผ๋ก ์งํ"
|
| 800 |
+
]
|
| 801 |
+
return {
|
| 802 |
+
"needs_refinement": False,
|
| 803 |
+
"intermediate_steps": steps_delta
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
# 1์ฐจ ํ๊ฐ: ๊ฐ์
|
| 807 |
+
if result_count < 2:
|
| 808 |
+
steps_delta = [
|
| 809 |
+
f"โ ๏ธ ๊ฒ์ ๊ฒฐ๊ณผ ๋ถ์กฑ ({result_count}๊ฐ < 2๊ฐ), ์ฟผ๋ฆฌ ๊ฐ์ ํ์"
|
| 810 |
+
]
|
| 811 |
+
return {
|
| 812 |
+
"needs_refinement": True,
|
| 813 |
+
"intermediate_steps": steps_delta
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
# 2์ฐจ ํ๊ฐ: ํ์ง
|
| 817 |
+
scored_results = [r for r in search_results if r.relevance_score is not None]
|
| 818 |
+
|
| 819 |
+
if scored_results:
|
| 820 |
+
avg_score = sum(r.relevance_score for r in scored_results) / len(scored_results)
|
| 821 |
+
|
| 822 |
+
if avg_score < 0.5:
|
| 823 |
+
steps_delta = [
|
| 824 |
+
f"โ ๏ธ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ง ๋ถ์กฑ (ํ๊ท ์ ์: {avg_score:.2f} < 0.5), ์ฟผ๋ฆฌ ๊ฐ์ ํ์"
|
| 825 |
+
]
|
| 826 |
+
return {
|
| 827 |
+
"needs_refinement": True,
|
| 828 |
+
"intermediate_steps": steps_delta
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
steps_delta = [
|
| 832 |
+
f"โ
๊ฒ์ ๊ฒฐ๊ณผ ์ถฉ๋ถ ({result_count}๊ฐ, ํ๊ท ์ ์: {avg_score:.2f}), ํํฐ๋ง ๋จ๊ณ๋ก ์งํ"
|
| 833 |
+
]
|
| 834 |
+
else:
|
| 835 |
+
steps_delta = [
|
| 836 |
+
f"โ
๊ฒ์ ๊ฒฐ๊ณผ ์ถฉ๋ถ ({result_count}๊ฐ), ํํฐ๋ง ๋จ๊ณ๋ก ์งํ"
|
| 837 |
+
]
|
| 838 |
+
|
| 839 |
+
return {
|
| 840 |
+
"needs_refinement": False,
|
| 841 |
+
"intermediate_steps": steps_delta
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
@trace_node("refine_search")
|
| 846 |
+
def refine_search_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 847 |
+
"""
|
| 848 |
+
๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํฉ๋๋ค.
|
| 849 |
+
|
| 850 |
+
๐ง CRITICAL:
|
| 851 |
+
- user_question์ ์ง์ ์
๋ฐ์ดํธํ์ง ์๊ณ , refined_question์ ์ ์ฅ
|
| 852 |
+
- ๋ถ๋ชจ AgentState์ ์ถฉ๋ ๋ฐฉ์ง๋ฅผ ์ํด WorkerState ํ๋๋ง ๋ฐํ
|
| 853 |
+
- โ ์ ๋ ๋ฐํํ๋ฉด ์ ๋๋ ๊ฒ๋ค: user_question, messages, final_answer
|
| 854 |
+
"""
|
| 855 |
+
# ๐ง [FIX] ๋ณ์ ์ ๊ทผ ์์
|
| 856 |
+
user_question = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 857 |
+
original_question = state.original_question or user_question
|
| 858 |
+
result_count = len(state.search_results)
|
| 859 |
+
|
| 860 |
+
logger.info("๊ฒ์ ์ฟผ๋ฆฌ ๊ฐ์ ์ค: %s (%d๊ฐ ๊ฒฐ๊ณผ)", user_question[:50], result_count)
|
| 861 |
+
|
| 862 |
+
refinement_prompt = f"""๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํฉ๋๋ค. ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํ์ธ์.
|
| 863 |
+
|
| 864 |
+
์๋ณธ ์ง๋ฌธ: {user_question}
|
| 865 |
+
ํ์ฌ ๊ฒฐ๊ณผ ์: {result_count}๊ฐ (๋ชฉํ: 2๊ฐ ์ด์)
|
| 866 |
+
|
| 867 |
+
๊ฐ์ ์ ๋ต (ํ๋ ์ ํ):
|
| 868 |
+
1. MORE_SPECIFIC: ๊ธฐ์ ์ ์ธ๋ถ์ฌํญ ์ถ๊ฐ
|
| 869 |
+
2. MORE_GENERAL: ๋ ๋์ ์ฉ์ด ์ฌ์ฉ
|
| 870 |
+
3. TRANSLATE: ์ธ์ด ๋ณํ
|
| 871 |
+
|
| 872 |
+
๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์:
|
| 873 |
+
{{
|
| 874 |
+
"new_query": "๊ฐ์ ๋ ๊ฒ์ ์ฟผ๋ฆฌ",
|
| 875 |
+
"strategy": "MORE_SPECIFIC|MORE_GENERAL|TRANSLATE",
|
| 876 |
+
"reasoning": "์ด ์ ๋ต์ ์ ํํ ์ด์ 1-2๋ฌธ์ฅ"
|
| 877 |
+
}}
|
| 878 |
+
|
| 879 |
+
JSON ์ธ์ ๋ค๋ฅธ ํ
์คํธ๋ ํฌํจํ์ง ๋ง์ธ์."""
|
| 880 |
+
|
| 881 |
+
try:
|
| 882 |
+
import json
|
| 883 |
+
|
| 884 |
+
messages_to_llm = [HumanMessage(content=refinement_prompt)]
|
| 885 |
+
response = llm.invoke(messages_to_llm)
|
| 886 |
+
|
| 887 |
+
response_text = response.content.strip()
|
| 888 |
+
if "```json" in response_text:
|
| 889 |
+
response_text = response_text.split("```json")[1].split("```")[0].strip()
|
| 890 |
+
elif "```" in response_text:
|
| 891 |
+
response_text = response_text.split("```")[1].split("```")[0].strip()
|
| 892 |
+
|
| 893 |
+
refinement_data = json.loads(response_text)
|
| 894 |
+
|
| 895 |
+
new_query = refinement_data.get("new_query", user_question)
|
| 896 |
+
strategy = refinement_data.get("strategy", "MORE_GENERAL")
|
| 897 |
+
reasoning = refinement_data.get("reasoning", "")
|
| 898 |
+
|
| 899 |
+
steps_delta = [
|
| 900 |
+
f"๐ ์ฟผ๋ฆฌ ๊ฐ์ : {strategy}",
|
| 901 |
+
f" ์ด์ : {user_question[:50]}...",
|
| 902 |
+
f" ์ดํ: {new_query[:50]}...",
|
| 903 |
+
f" ์ด์ : {reasoning}"
|
| 904 |
+
]
|
| 905 |
+
|
| 906 |
+
logger.info("์ฟผ๋ฆฌ ๊ฐ์ ์๋ฃ: %s โ %s", user_question[:30], new_query[:30])
|
| 907 |
+
|
| 908 |
+
# ๐ง CRITICAL: WorkerState ํ๋๋ง ๋ฐํ (๋ถ๋ชจ AgentState์ ์ถฉ๋ ๋ฐฉ์ง)
|
| 909 |
+
return {
|
| 910 |
+
"refined_question": new_query, # โ
WorkerState ํ๋
|
| 911 |
+
"original_question": original_question, # โ
WorkerState ํ๋
|
| 912 |
+
"refinement_count": state.refinement_count + 1, # โ
WorkerState ํ๋
|
| 913 |
+
"search_results": [], # โ
WorkerState ํ๋ (reducer ์์)
|
| 914 |
+
"intermediate_steps": steps_delta # โ
WorkerState ํ๋
|
| 915 |
+
|
| 916 |
+
# โ ์ ๋ ๋ฐํํ๋ฉด ์ ๋๋ ๊ฒ๋ค:
|
| 917 |
+
# "user_question": ..., # ๋ถ๋ชจ AgentState์ ์ถฉ๋!
|
| 918 |
+
# "messages": ..., # ๋ถ๋ชจ AgentState์ ์ถฉ๋!
|
| 919 |
+
# "final_answer": ..., # ๋๋ฌด ์ด๋ฅธ ์์ !
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
except Exception as e:
|
| 923 |
+
logger.error("์ฟผ๋ฆฌ ๊ฐ์ ์คํจ: %s", e, exc_info=True)
|
| 924 |
+
|
| 925 |
+
fallback_query = user_question + " tutorial example"
|
| 926 |
+
|
| 927 |
+
steps_delta = [
|
| 928 |
+
f"โ ๏ธ ์ฟผ๋ฆฌ ๊ฐ์ ์คํจ, ๊ธฐ๋ณธ ์ ๋ต ์ฌ์ฉ",
|
| 929 |
+
f" ์ดํ: {fallback_query}"
|
| 930 |
+
]
|
| 931 |
+
|
| 932 |
+
# ๐ง CRITICAL: WorkerState ํ๋๋ง ๋ฐํ
|
| 933 |
+
return {
|
| 934 |
+
"refined_question": fallback_query, # โ
WorkerState ํ๋
|
| 935 |
+
"original_question": original_question, # โ
WorkerState ํ๋
|
| 936 |
+
"refinement_count": state.refinement_count + 1, # โ
WorkerState ํ๋
|
| 937 |
+
"search_results": [], # โ
WorkerState ํ๋ (reducer ์์)
|
| 938 |
+
"intermediate_steps": steps_delta # โ
WorkerState ํ๋
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
|
| 942 |
+
@trace_node("filter_and_score")
|
| 943 |
+
def filter_and_score_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 944 |
+
"""๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ํํฐ๋งํ๊ณ ๊ด๋ จ๋ ์ ์๋ฅผ ๋งค๊น๋๋ค."""
|
| 945 |
+
search_results = state.search_results
|
| 946 |
+
logger.info("๊ฒ์ ๊ฒฐ๊ณผ ํํฐ๋ง ์ค: %d๊ฐ", len(search_results))
|
| 947 |
+
|
| 948 |
+
# ๊ธฐ๋ณธ ํํฐ๋ง
|
| 949 |
+
filtered = [
|
| 950 |
+
r for r in search_results
|
| 951 |
+
if r.content and len(r.content) >= 50 and r.url
|
| 952 |
+
]
|
| 953 |
+
|
| 954 |
+
logger.info("๊ธฐ๋ณธ ํํฐ๋ง ํ: %d๊ฐ ๊ฒฐ๊ณผ", len(filtered))
|
| 955 |
+
|
| 956 |
+
# ์์ 5๊ฐ ๊ฒฐ๊ณผ๋ง LLM์ผ๋ก ์ ์ ๋งค๊ธฐ๊ธฐ
|
| 957 |
+
# ๐ง [FIX] scoring_prompt ๋ด๋ถ์์ ์ง๋ฌธ ์ฐธ์กฐ ์ ์์
|
| 958 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 959 |
+
|
| 960 |
+
for result in filtered[:5]:
|
| 961 |
+
if result.relevance_score is None:
|
| 962 |
+
try:
|
| 963 |
+
scoring_prompt = f"""์ง๋ฌธ: {current_q}
|
| 964 |
+
|
| 965 |
+
๊ฒ์ ๊ฒฐ๊ณผ: {result.content[:500]}
|
| 966 |
+
|
| 967 |
+
์ด ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ง๋ฌธ์ ์ผ๋ง๋ ๊ด๋ จ์ด ์๋์ง 0.0์์ 1.0 ์ฌ์ด์ ์ ์๋ก ํ๊ฐํ์ธ์.
|
| 968 |
+
์ ์๋ง ์ซ์๋ก ๋ตํ์ธ์. (์: 0.8)"""
|
| 969 |
+
|
| 970 |
+
response = llm.invoke([HumanMessage(content=scoring_prompt)])
|
| 971 |
+
score_str = response.content.strip()
|
| 972 |
+
result.relevance_score = float(score_str)
|
| 973 |
+
|
| 974 |
+
except Exception as e:
|
| 975 |
+
logger.warning("์ ์ ๋งค๊ธฐ๊ธฐ ์คํจ: %s", e)
|
| 976 |
+
result.relevance_score = 0.5
|
| 977 |
+
|
| 978 |
+
# ๊ด๋ จ๋ ์์ผ๋ก ์ ๋ ฌ
|
| 979 |
+
filtered.sort(key=lambda r: r.relevance_score or 0, reverse=True)
|
| 980 |
+
|
| 981 |
+
# ์์ 5๊ฐ๋ง ์ ์ง
|
| 982 |
+
top_results = filtered[:5]
|
| 983 |
+
|
| 984 |
+
subtask_results = dict(state.subtask_results)
|
| 985 |
+
subtask_results["filtered_results"] = [r.model_dump() for r in top_results]
|
| 986 |
+
|
| 987 |
+
steps_delta = [f"โ๏ธ ํํฐ๋ง ์๋ฃ: {len(top_results)}๊ฐ ๊ฒฐ๊ณผ ์ ํ"]
|
| 988 |
+
|
| 989 |
+
logger.info("ํํฐ๋ง ์๋ฃ: %d๊ฐ ๊ฒฐ๊ณผ", len(top_results))
|
| 990 |
+
|
| 991 |
+
return {
|
| 992 |
+
"subtask_results": subtask_results,
|
| 993 |
+
"intermediate_steps": steps_delta
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
|
| 997 |
+
@trace_node("summarize_results")
|
| 998 |
+
def summarize_results_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 999 |
+
"""ํํฐ๋ง๋ ๊ฐ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ณด ๊ฐ๋ฐ์๊ฐ ์ดํดํ๊ธฐ ์ฝ๊ฒ ์์ฝํฉ๋๋ค."""
|
| 1000 |
+
subtask_results = state.subtask_results
|
| 1001 |
+
filtered_results = subtask_results.get("filtered_results", [])
|
| 1002 |
+
logger.info("๊ฒ์ ๊ฒฐ๊ณผ ์์ฝ ์ค: %d๊ฐ", len(filtered_results))
|
| 1003 |
+
|
| 1004 |
+
summaries = []
|
| 1005 |
+
|
| 1006 |
+
for result_dict in filtered_results:
|
| 1007 |
+
try:
|
| 1008 |
+
summary_prompt = f"""๋ค์ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ณด ๊ฐ๋ฐ์๊ฐ ์ดํดํ๊ธฐ ์ฝ๊ฒ 2-3๋ฌธ์ฅ์ผ๋ก ์์ฝํ์ธ์:
|
| 1009 |
+
|
| 1010 |
+
์ถ์ฒ: {result_dict['source']}
|
| 1011 |
+
๋ด์ฉ: {result_dict['content'][:1000]}
|
| 1012 |
+
|
| 1013 |
+
ํต์ฌ ๋ด์ฉ๋ง ๊ฐ๋จ๋ช
๋ฃํ๊ฒ ์์ฝํ์ธ์."""
|
| 1014 |
+
|
| 1015 |
+
response = llm.invoke([HumanMessage(content=summary_prompt)])
|
| 1016 |
+
|
| 1017 |
+
summaries.append({
|
| 1018 |
+
"source": result_dict['source'],
|
| 1019 |
+
"url": result_dict['url'],
|
| 1020 |
+
"summary": response.content.strip(),
|
| 1021 |
+
"relevance": result_dict.get('relevance_score', 0.5)
|
| 1022 |
+
})
|
| 1023 |
+
|
| 1024 |
+
except Exception as e:
|
| 1025 |
+
logger.error("์์ฝ ์คํจ: %s", e)
|
| 1026 |
+
|
| 1027 |
+
updated_subtask_results = dict(subtask_results)
|
| 1028 |
+
updated_subtask_results["summaries"] = summaries
|
| 1029 |
+
|
| 1030 |
+
steps_delta = [f"๐ ์์ฝ ์๋ฃ: {len(summaries)}๊ฐ ๊ฒฐ๊ณผ"]
|
| 1031 |
+
|
| 1032 |
+
logger.info("์์ฝ ์๋ฃ: %d๊ฐ", len(summaries))
|
| 1033 |
+
|
| 1034 |
+
return {
|
| 1035 |
+
"subtask_results": updated_subtask_results,
|
| 1036 |
+
"intermediate_steps": steps_delta
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
|
| 1040 |
+
@trace_node("generate_answer")
|
| 1041 |
+
async def generate_answer_node(state: Union[AgentState, WorkerState]) -> dict:
|
| 1042 |
+
"""
|
| 1043 |
+
์์ฝ๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ต์ข
๋ต๋ณ์ ์์ฑํฉ๋๋ค.
|
| 1044 |
+
|
| 1045 |
+
์์ ์ฌํญ:
|
| 1046 |
+
1. ๋ค์ค ์ง๋ฌธ ๋ชจ๋์์๋ ์บ์ ์ ์ฅ ๋ก์ง์ด ์คํ๋๋๋ก ์์ ๋ณ๊ฒฝ
|
| 1047 |
+
2. ๋จ์ผ ์ง๋ฌธ ๋ชจ๋์์ AIMessage ๋ฐํ (ํ์คํ ๋ฆฌ ์ ์ฅ)
|
| 1048 |
+
"""
|
| 1049 |
+
subtask_results = state.subtask_results
|
| 1050 |
+
summaries = subtask_results.get("summaries", [])
|
| 1051 |
+
intent = state.detected_intent or "learning"
|
| 1052 |
+
|
| 1053 |
+
# ๋ณ์ ์ ๊ทผ
|
| 1054 |
+
current_q = state.processing_question if isinstance(state, WorkerState) else state.user_question
|
| 1055 |
+
|
| 1056 |
+
logger.info("์ต์ข
๋ต๋ณ ์์ฑ ์ค: %s (์ง๋ฌธ: %s)", intent, current_q[:30])
|
| 1057 |
+
|
| 1058 |
+
# 1. ์๋๋ณ ํ๋กฌํํธ ํ
ํ๋ฆฟ
|
| 1059 |
+
templates = {
|
| 1060 |
+
"debugging": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋๋ฒ๊น
์ง๋ฌธ์ ๋ต๋ณํ์ธ์:
|
| 1061 |
+
|
| 1062 |
+
์ง๋ฌธ: {question}
|
| 1063 |
+
|
| 1064 |
+
์์ง๋ ์ ๋ณด:
|
| 1065 |
+
{summaries}
|
| 1066 |
+
|
| 1067 |
+
๋ต๋ณ ๊ตฌ์กฐ:
|
| 1068 |
+
1. ๋ฌธ์ ์ ์
|
| 1069 |
+
2. ๋ฐ์ ์์ธ
|
| 1070 |
+
3. ํด๊ฒฐ ๋ฐฉ๋ฒ (์ฝ๋ ์์ ํฌํจ)
|
| 1071 |
+
4. ์ฃผ์์ฌํญ
|
| 1072 |
+
5. ์ฐธ๊ณ ์๋ฃ
|
| 1073 |
+
|
| 1074 |
+
์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์.""",
|
| 1075 |
+
|
| 1076 |
+
"learning": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ํ์ต ์ง๋ฌธ์ ๋ต๋ณํ์ธ์:
|
| 1077 |
+
|
| 1078 |
+
์ง๋ฌธ: {question}
|
| 1079 |
+
|
| 1080 |
+
์์ง๋ ์ ๋ณด:
|
| 1081 |
+
{summaries}
|
| 1082 |
+
|
| 1083 |
+
๋ต๋ณ ๊ตฌ์กฐ:
|
| 1084 |
+
1. ๊ฐ๋
์ค๋ช
(๊ฐ๋จ๋ช
๋ฃ)
|
| 1085 |
+
2. ๋์ ์๋ฆฌ
|
| 1086 |
+
3. ์์ ์ฝ๋ (์ฃผ์ํฌํจ)
|
| 1087 |
+
4. ์ค๋ฌด ํ์ฉ ํ
|
| 1088 |
+
5. ์ถ๊ฐ ํ์ต ์๋ฃ
|
| 1089 |
+
|
| 1090 |
+
์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์.""",
|
| 1091 |
+
|
| 1092 |
+
"code_review": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ฝ๋ ๋ฆฌ๋ทฐ ์ง๋ฌธ์ ๋ต๋ณํ์ธ์:
|
| 1093 |
+
|
| 1094 |
+
์ง๋ฌธ: {question}
|
| 1095 |
+
|
| 1096 |
+
์์ง๋ ์ ๋ณด:
|
| 1097 |
+
{summaries}
|
| 1098 |
+
|
| 1099 |
+
๋ต๋ณ ๊ตฌ์กฐ:
|
| 1100 |
+
1. ํ์ฌ ์ ๊ทผ ๋ฐฉ์ ๋ถ์
|
| 1101 |
+
2. ๊ฐ์ ํฌ์ธํธ
|
| 1102 |
+
3. ๋ฆฌํฉํ ๋ง ์์
|
| 1103 |
+
4. ๋ฒ ์คํธ ํ๋ํฐ์ค
|
| 1104 |
+
5. ์ฐธ๊ณ ํจํด
|
| 1105 |
+
|
| 1106 |
+
์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์."""
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
template = templates.get(intent, templates["learning"])
|
| 1110 |
+
|
| 1111 |
+
# 2. ์์ฝ ํ
์คํธ ํฌ๋งทํ
|
| 1112 |
+
summaries_text = "\n\n".join([
|
| 1113 |
+
f"์ถ์ฒ: {s['source']} ({s['url']})\n์์ฝ: {s['summary']}"
|
| 1114 |
+
for s in summaries
|
| 1115 |
+
])
|
| 1116 |
+
|
| 1117 |
+
# 3. ์ด์ ๋ํ ๋งฅ๋ฝ ์ถ๊ฐ (Context Pollution ๋ฐฉ์ง: ์ต๊ทผ 1๊ฐ๋ง ์ฐธ๊ณ ์ฉ์ผ๋ก)
|
| 1118 |
+
context_prefix = ""
|
| 1119 |
+
messages_history = state.messages
|
| 1120 |
+
if messages_history and len(messages_history) > 1:
|
| 1121 |
+
# ๊ฒ์ ๊ธฐ๋ฐ ๋ต๋ณ์ด๋ฏ๋ก ์ด์ ๋ํ๋ ์์ฃผ ์ต์ํ๋ง ์ฐธ์กฐ (์ง์ 1๊ฐ)
|
| 1122 |
+
prev_msg = messages_history[-2] if len(messages_history) >= 2 else None
|
| 1123 |
+
if prev_msg:
|
| 1124 |
+
context_prefix = f"์ด์ ๋ํ ๋งฅ๋ฝ(์ฐธ๊ณ ): {prev_msg.content[:200]}...\n---\n"
|
| 1125 |
+
|
| 1126 |
+
final_prompt = (context_prefix + template).format(
|
| 1127 |
+
question=(state.original_question or current_q),
|
| 1128 |
+
summaries=summaries_text
|
| 1129 |
+
)
|
| 1130 |
+
|
| 1131 |
+
updates = {}
|
| 1132 |
+
steps_delta: List[str] = []
|
| 1133 |
+
|
| 1134 |
+
try:
|
| 1135 |
+
# 4. LLM ํธ์ถ
|
| 1136 |
+
response = llm.invoke([HumanMessage(content=final_prompt)])
|
| 1137 |
+
final_answer = response.content.strip()
|
| 1138 |
+
|
| 1139 |
+
# 5. ์บ์ ์ ์ฅ ๋ก์ง (DRY - ์ค๋ณต ๋ฐฉ์ง ํจ์)
|
| 1140 |
+
should_cache = state.should_cache if state.should_cache is not None else True
|
| 1141 |
+
canonical_question = state.canonical_question
|
| 1142 |
+
qtype = state.question_type or "independent"
|
| 1143 |
+
question_to_cache = canonical_question or current_q
|
| 1144 |
+
|
| 1145 |
+
async def _try_cache_save():
|
| 1146 |
+
"""์กฐ๊ฑด ์ถฉ์กฑ ์ Qdrant์ ์บ์ ์ ์ฅ"""
|
| 1147 |
+
if should_cache and qtype in ["new_topic", "independent"]:
|
| 1148 |
+
try:
|
| 1149 |
+
await qdrant_manager.save_to_cache(
|
| 1150 |
+
question=question_to_cache,
|
| 1151 |
+
answer=final_answer
|
| 1152 |
+
)
|
| 1153 |
+
logger.info("โ
์บ์ ์ ์ฅ ์๋ฃ: %s", question_to_cache[:30])
|
| 1154 |
+
return True
|
| 1155 |
+
except Exception as cache_err:
|
| 1156 |
+
logger.error("์บ์ ์ ์ฅ ์คํจ: %s", cache_err)
|
| 1157 |
+
return False
|
| 1158 |
+
return False
|
| 1159 |
+
|
| 1160 |
+
# 6. ๊ฒฐ๊ณผ ๋ฐํ ๋ฐ ๋ถ๊ธฐ ์ฒ๋ฆฌ
|
| 1161 |
+
is_multi = isinstance(state, WorkerState) and state.worker_is_multi
|
| 1162 |
+
|
| 1163 |
+
if is_multi:
|
| 1164 |
+
# [ํต์ฌ] ๋ค์ค ์ง๋ฌธ ๋ชจ๋: Returnํ๊ธฐ '์ ์' ์บ์ ์ ์ฅ ์๋
|
| 1165 |
+
await _try_cache_save()
|
| 1166 |
+
|
| 1167 |
+
logger.info("๋ค์ค ์ง๋ฌธ ๋ชจ๋: ๋ต๋ณ์ multi_answers์ ์ถ๊ฐ")
|
| 1168 |
+
return {
|
| 1169 |
+
"multi_answers": [{
|
| 1170 |
+
"index": state.worker_idx,
|
| 1171 |
+
"question": state.worker_sub_text or current_q,
|
| 1172 |
+
"answer": final_answer
|
| 1173 |
+
}]
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
else:
|
| 1177 |
+
# ๋จ์ผ ์ง๋ฌธ ๋ชจ๋
|
| 1178 |
+
updates["final_answer"] = final_answer
|
| 1179 |
+
# [ํต์ฌ] ๋ํ ํ์คํ ๋ฆฌ์ AI ๋ต๋ณ ์ถ๊ฐ
|
| 1180 |
+
updates["messages"] = [AIMessage(content=final_answer)]
|
| 1181 |
+
|
| 1182 |
+
# ์บ์ ์ ์ฅ ์๋
|
| 1183 |
+
saved = await _try_cache_save()
|
| 1184 |
+
|
| 1185 |
+
if saved:
|
| 1186 |
+
steps_delta.append(f"โ
์ต์ข
๋ต๋ณ ์์ฑ ์๋ฃ (๊ธธ์ด: {len(final_answer)}์)")
|
| 1187 |
+
steps_delta.append(f"๐พ ์บ์ ์ ์ฅ ์๋ฃ (์ง๋ฌธ: {question_to_cache[:50]}...)")
|
| 1188 |
+
else:
|
| 1189 |
+
steps_delta.append(f"โ
์ต์ข
๋ต๋ณ ์์ฑ ์๋ฃ (๊ธธ์ด: {len(final_answer)}์)")
|
| 1190 |
+
steps_delta.append("โ ๏ธ ์บ์ ์ ์ฅ ์๋ต (๋
๋ฆฝ์ ์ด์ง ์๊ฑฐ๋ ์ผํ์ฑ ์ง๋ฌธ)")
|
| 1191 |
+
logger.info("์ต์ข
๋ต๋ณ ์์ฑ ์๋ฃ (์บ์ ์ ์ฅ ์๋ต)")
|
| 1192 |
+
|
| 1193 |
+
updates["intermediate_steps"] = steps_delta
|
| 1194 |
+
return updates
|
| 1195 |
+
|
| 1196 |
+
except Exception as e:
|
| 1197 |
+
logger.error("๋ต๋ณ ์์ฑ ์คํจ: %s", e, exc_info=True)
|
| 1198 |
+
|
| 1199 |
+
is_multi = isinstance(state, WorkerState) and state.worker_is_multi
|
| 1200 |
+
if is_multi:
|
| 1201 |
+
return {
|
| 1202 |
+
"multi_answers": [{
|
| 1203 |
+
"index": state.worker_idx,
|
| 1204 |
+
"question": state.worker_sub_text or current_q,
|
| 1205 |
+
"answer": "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์."
|
| 1206 |
+
}]
|
| 1207 |
+
}
|
| 1208 |
+
else:
|
| 1209 |
+
updates["final_answer"] = "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์."
|
| 1210 |
+
steps_delta.append(f"โ ๋ต๋ณ ์์ฑ ์คํจ: {str(e)}")
|
| 1211 |
+
updates["intermediate_steps"] = steps_delta
|
| 1212 |
+
return updates
|
CodeWeaver/src/agent/state.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional, Literal, Tuple, Annotated
|
| 2 |
+
from operator import add
|
| 3 |
+
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from langchain_core.messages import BaseMessage
|
| 6 |
+
from langgraph.graph import add_messages
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_STEPS_RESET_TOKEN = "__RESET_STEPS__"
|
| 10 |
+
_MULTI_ANS_RESET_TOKEN = "__RESET_MULTI_ANS__"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def merge_intermediate_steps(old: List[str], new: List[str]) -> List[str]:
|
| 14 |
+
"""intermediate_steps reducer."""
|
| 15 |
+
if not new:
|
| 16 |
+
return old
|
| 17 |
+
if new[0] == _STEPS_RESET_TOKEN:
|
| 18 |
+
return new[1:]
|
| 19 |
+
return old + new
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def merge_multi_answers(old: List[Dict[str, Any]], new: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 23 |
+
"""multi_answers reducer."""
|
| 24 |
+
if not new:
|
| 25 |
+
return old
|
| 26 |
+
head = new[0]
|
| 27 |
+
if isinstance(head, dict) and head.get("__token__") == _MULTI_ANS_RESET_TOKEN:
|
| 28 |
+
return new[1:]
|
| 29 |
+
return old + new
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def merge_search_results(old: List["SearchResult"], new: List["SearchResult"]) -> List["SearchResult"]:
|
| 33 |
+
"""
|
| 34 |
+
search_results reducer.
|
| 35 |
+
๋ณ๋ ฌ ๊ฒ์ ๋
ธ๋๋ค์ด ๋์์ search_results๋ฅผ ์
๋ฐ์ดํธํ ์ ์๋๋ก ๋ณํฉ ๋ก์ง ์ ๊ณต.
|
| 36 |
+
"""
|
| 37 |
+
return old + new
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SearchResult(BaseModel):
|
| 41 |
+
"""๊ฒ์ ๋๋ฉ์ธ์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ๋จ์ผ ๊ฒ์ ๊ฒฐ๊ณผ ๋ชจ๋ธ."""
|
| 42 |
+
source: str = Field(..., description="๊ฒ์ ์ถ์ฒ")
|
| 43 |
+
content: str = Field(..., description="๊ฒ์ ๊ฒฐ๊ณผ์ ํต์ฌ ๋ด์ฉ")
|
| 44 |
+
url: Optional[str] = Field(default=None, description="์๋ณธ ์ถ์ฒ URL")
|
| 45 |
+
relevance_score: Optional[float] = Field(default=None, description="๊ด๋ จ๋ ์ ์")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class AgentState(BaseModel):
|
| 49 |
+
"""๋ถ๋ชจ ๊ทธ๋ํ ์ ์ฉ ์ํ."""
|
| 50 |
+
|
| 51 |
+
# Core fields
|
| 52 |
+
user_question: str = Field(default="", description="์ฌ์ฉ์์ ์๋ณธ ์ง๋ฌธ")
|
| 53 |
+
messages: Annotated[List[BaseMessage], add_messages] = Field(
|
| 54 |
+
default_factory=list,
|
| 55 |
+
description="๋ํ ๋ฉ์์ง ํ์คํ ๋ฆฌ"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Final output
|
| 59 |
+
final_answer: Optional[str] = Field(default=None, description="์ต์ข
์์ฑ๋ ๋ต๋ณ")
|
| 60 |
+
|
| 61 |
+
# Debugging/tracing
|
| 62 |
+
intermediate_steps: Annotated[List[str], merge_intermediate_steps] = Field(
|
| 63 |
+
default_factory=list,
|
| 64 |
+
description="์คํ ๋จ๊ณ๋ณ ๋ก๊ทธ"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Planning
|
| 68 |
+
plan: Optional[Dict[str, Any]] = Field(
|
| 69 |
+
default=None,
|
| 70 |
+
description="์ง๋ฌธ ๋ถํด ๊ณํ"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Multi-question handling
|
| 74 |
+
is_multi_question: bool = Field(default=False)
|
| 75 |
+
sub_question_index: int = Field(default=0)
|
| 76 |
+
sub_question_text: Optional[str] = Field(default=None)
|
| 77 |
+
original_multi_question: Optional[str] = Field(default=None)
|
| 78 |
+
multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = Field(
|
| 79 |
+
default_factory=list,
|
| 80 |
+
description="๋ค์ค ์ง๋ฌธ์ ๊ฐ ๋ต๋ณ ๋ฆฌ์คํธ"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
class Config:
|
| 84 |
+
arbitrary_types_allowed = True
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class WorkerState(BaseModel):
|
| 88 |
+
"""
|
| 89 |
+
์๋ธ๊ทธ๋ํ ์ ์ฉ ์ํ.
|
| 90 |
+
๋ถ๋ชจ AgentState์ ํค ์ด๋ฆ์ด ๊ฒน์น์ง ์๋๋ก ์ฃผ์ํด์ผ ํฉ๋๋ค.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
# === ์
๋ ฅ (๋ถ๋ชจ๋ก๋ถํฐ ๋ฐ์) ===
|
| 94 |
+
processing_question: str = Field(default="", description="ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ์ง๋ฌธ")
|
| 95 |
+
messages: List[BaseMessage] = Field(default_factory=list, description="๋ํ ํ์คํ ๋ฆฌ")
|
| 96 |
+
|
| 97 |
+
# ๐ง [FIX] ๋ถ๋ชจ ์ํ์ ์ถฉ๋ ๋ฐฉ์ง๋ฅผ ์ํด ์ด๋ฆ ๋ณ๊ฒฝ (worker_ ์ ๋์ฌ)
|
| 98 |
+
worker_is_multi: bool = Field(default=False)
|
| 99 |
+
worker_idx: int = Field(default=0)
|
| 100 |
+
worker_sub_text: Optional[str] = Field(default=None)
|
| 101 |
+
|
| 102 |
+
# === ์๋ธ๊ทธ๋ํ ๋ด๋ถ ์ ์ฉ ํ๋ ===
|
| 103 |
+
# (์ด ํ๋๋ค์ ์๋ธ๊ทธ๋ํ ๋ด๋ถ์์๋ง ์ฌ์ฉ, ๋ถ๋ชจ์๊ฒ ์ ๋ฌ ์ ๋จ)
|
| 104 |
+
question_type: Optional[Literal["clarification", "new_topic", "independent"]] = None
|
| 105 |
+
should_cache: Optional[bool] = None
|
| 106 |
+
canonical_question: Optional[str] = None
|
| 107 |
+
analysis_reasoning: Optional[str] = None
|
| 108 |
+
cached_result: Optional[str] = None
|
| 109 |
+
detected_intent: Optional[Literal["debugging", "learning", "code_review"]] = None
|
| 110 |
+
|
| 111 |
+
# ๊ฒ์ ๊ฒฐ๊ณผ (๋ณ๋ ฌ ์
๋ฐ์ดํธ ๊ฐ๋ฅํ๋๋ก reducer ์ ์ฉ)
|
| 112 |
+
search_results: Annotated[List[SearchResult], merge_search_results] = Field(
|
| 113 |
+
default_factory=list,
|
| 114 |
+
description="๋ณ๋ ฌ ๊ฒ์ ๊ฒฐ๊ณผ (reducer๋ก ์๋ ๋ณํฉ)"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
subtask_results: Dict[str, Any] = Field(default_factory=dict)
|
| 118 |
+
|
| 119 |
+
# ์ฟผ๋ฆฌ ๊ฐ์ (์ด ํ๋๋ค์ refine_search_node๋ง ์
๋ฐ์ดํธ)
|
| 120 |
+
needs_refinement: bool = False
|
| 121 |
+
refinement_count: int = 0
|
| 122 |
+
original_question: Optional[str] = None
|
| 123 |
+
refined_question: Optional[str] = None # ๐ง ๊ฐ์ ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ณ๋ ํ๋๋ก ๊ด๋ฆฌ
|
| 124 |
+
|
| 125 |
+
# ๐ง ์๋ธ๊ทธ๋ํ ๋ด๋ถ ๋ก๊ทธ (๋ถ๋ชจ์๊ฒ ์ ๋ฌ ์ ๋จ!)
|
| 126 |
+
intermediate_steps: List[str] = Field(
|
| 127 |
+
default_factory=list,
|
| 128 |
+
description="์๋ธ๊ทธ๋ํ ๋ด๋ถ ๋ก๊ทธ (๋ถ๋ชจ์ ์ ๋ฌํ์ง ์์)"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# === ์ถ๋ ฅ (๋ถ๋ชจ์๊ฒ ์ ๋ฌ๋ ํ๋) ===
|
| 132 |
+
# ์ด ํ๋๋ค์ ๋ถ๋ชจ AgentState์๋ ์กด์ฌํ๋ฉฐ, Reducer๏ฟฝ๏ฟฝ๏ฟฝ ์๊ฑฐ๋ ์ถฉ๋์ด ํ์ฉ๋๋ ํ๋์ฌ์ผ ํจ
|
| 133 |
+
final_answer: Optional[str] = None
|
| 134 |
+
|
| 135 |
+
multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = Field(
|
| 136 |
+
default_factory=list,
|
| 137 |
+
description="๋ค์ค ์ง๋ฌธ ๋ต๋ณ์ฉ"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
class Config:
|
| 141 |
+
arbitrary_types_allowed = True
|
CodeWeaver/src/tools/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .search_tools import (
|
| 2 |
+
search_github,
|
| 3 |
+
search_official_docs,
|
| 4 |
+
search_stackoverflow,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"search_stackoverflow",
|
| 9 |
+
"search_github",
|
| 10 |
+
"search_official_docs",
|
| 11 |
+
]
|
| 12 |
+
|
CodeWeaver/src/tools/search_tools.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
from tavily import TavilyClient # type: ignore[import]
|
| 8 |
+
|
| 9 |
+
from src.agent.state import SearchResult
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def search_stackoverflow(query: str, limit: int = 3) -> List[SearchResult]:
|
| 16 |
+
"""Stack Overflow์์ ๊ด๋ จ ์ง๋ฌธ์ ๊ฒ์ํ๋ค.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
query: ๊ฒ์ ์ฟผ๋ฆฌ
|
| 20 |
+
limit: ๋ฐํํ ์ต๋ ๊ฒฐ๊ณผ ์
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
SearchResult ๋ฆฌ์คํธ (์คํจ ์ ๋น ๋ฆฌ์คํธ)
|
| 24 |
+
"""
|
| 25 |
+
if not query.strip():
|
| 26 |
+
logger.warning("Stack Overflow ๊ฒ์: ๋น ์ฟผ๋ฆฌ")
|
| 27 |
+
return []
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
url = "https://api.stackexchange.com/2.3/search/advanced"
|
| 31 |
+
params = {
|
| 32 |
+
"q": query,
|
| 33 |
+
"order": "desc",
|
| 34 |
+
"sort": "votes",
|
| 35 |
+
"site": "stackoverflow",
|
| 36 |
+
"pagesize": limit,
|
| 37 |
+
"filter": "withbody",
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
response = requests.get(url, params=params, timeout=10)
|
| 41 |
+
response.raise_for_status()
|
| 42 |
+
|
| 43 |
+
data = response.json()
|
| 44 |
+
items = data.get("items", [])
|
| 45 |
+
|
| 46 |
+
results = []
|
| 47 |
+
max_score = max((item.get("score", 0) for item in items), default=1)
|
| 48 |
+
|
| 49 |
+
for item in items:
|
| 50 |
+
title = item.get("title", "")
|
| 51 |
+
body = item.get("body", "")[:500] # ๋ณธ๋ฌธ ์ผ๋ถ๋ง ํฌํจ
|
| 52 |
+
content = f"{title}\n\n{body}"
|
| 53 |
+
|
| 54 |
+
score = item.get("score", 0)
|
| 55 |
+
# ์ ๊ทํ: 0-1 ๋ฒ์๋ก ๋ณํ
|
| 56 |
+
relevance = min(score / max(max_score, 1), 1.0) if max_score > 0 else 0.5
|
| 57 |
+
|
| 58 |
+
results.append(
|
| 59 |
+
SearchResult(
|
| 60 |
+
source="Stack Overflow",
|
| 61 |
+
content=content,
|
| 62 |
+
url=item.get("link"),
|
| 63 |
+
relevance_score=relevance,
|
| 64 |
+
)
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
logger.info("Stack Overflow ๊ฒ์ ์ฑ๊ณต: %d๊ฐ ๊ฒฐ๊ณผ", len(results))
|
| 68 |
+
|
| 69 |
+
# Rate limit ์ค์
|
| 70 |
+
time.sleep(1)
|
| 71 |
+
|
| 72 |
+
return results
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.error("Stack Overflow ๊ฒ์ ์คํจ: %s", e, exc_info=True)
|
| 76 |
+
return []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def search_github(query: str, limit: int = 3) -> List[SearchResult]:
|
| 80 |
+
"""GitHub์์ ๊ด๋ จ ์ฝ๋๋ฅผ ๊ฒ์ํ๋ค.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
query: ๊ฒ์ ์ฟผ๋ฆฌ
|
| 84 |
+
limit: ๋ฐํํ ์ต๋ ๊ฒฐ๊ณผ ์
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
SearchResult ๋ฆฌ์คํธ (์คํจ ์ ๋น ๋ฆฌ์คํธ)
|
| 88 |
+
"""
|
| 89 |
+
if not query.strip():
|
| 90 |
+
logger.warning("GitHub ๊ฒ์: ๋น ์ฟผ๋ฆฌ")
|
| 91 |
+
return []
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
url = "https://api.github.com/search/code"
|
| 95 |
+
|
| 96 |
+
# Python ์ฝ๋๋ก ์ ํ (์ธ์ด ๊ฐ์ง ๋ก์ง์ ์ถํ ํ์ฅ ๊ฐ๋ฅ)
|
| 97 |
+
search_query = f"{query} language:python"
|
| 98 |
+
|
| 99 |
+
params = {
|
| 100 |
+
"q": search_query,
|
| 101 |
+
"sort": "indexed",
|
| 102 |
+
"per_page": limit,
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
headers = {
|
| 106 |
+
"Accept": "application/vnd.github.v3+json",
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# GitHub ํ ํฐ์ด ์์ผ๋ฉด Authorization ํค๋ ์ถ๊ฐ
|
| 110 |
+
github_token = os.getenv("GITHUB_TOKEN", "").strip()
|
| 111 |
+
if github_token:
|
| 112 |
+
headers["Authorization"] = f"token {github_token}"
|
| 113 |
+
logger.debug("GitHub ํ ํฐ ์ฌ์ฉ (์ธ์ฆ๋ ์์ฒญ)")
|
| 114 |
+
else:
|
| 115 |
+
logger.warning(
|
| 116 |
+
"GITHUB_TOKEN์ด ์ค์ ๋์ง ์์ - rate limit ์ ํ์ (60 req/hr). "
|
| 117 |
+
"ํ ํฐ ์ค์ ์ 5,000 req/hr๋ก ์ฆ๊ฐ"
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
response = requests.get(url, params=params, headers=headers, timeout=10)
|
| 121 |
+
response.raise_for_status()
|
| 122 |
+
|
| 123 |
+
data = response.json()
|
| 124 |
+
items = data.get("items", [])
|
| 125 |
+
|
| 126 |
+
results = []
|
| 127 |
+
for item in items:
|
| 128 |
+
repo_name = item.get("repository", {}).get("full_name", "unknown")
|
| 129 |
+
path = item.get("path", "")
|
| 130 |
+
content = f"Repository: {repo_name}\nFile: {path}"
|
| 131 |
+
|
| 132 |
+
results.append(
|
| 133 |
+
SearchResult(
|
| 134 |
+
source="GitHub",
|
| 135 |
+
content=content,
|
| 136 |
+
url=item.get("html_url"),
|
| 137 |
+
relevance_score=0.8, # GitHub ๊ฒฐ๊ณผ๋ ์ผ๋ฐ์ ์ผ๋ก ๋์ ๊ด๋ จ๋
|
| 138 |
+
)
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
logger.info("GitHub ๊ฒ์ ์ฑ๊ณต: %d๊ฐ ๊ฒฐ๊ณผ", len(results))
|
| 142 |
+
|
| 143 |
+
# Rate limit ์ค์
|
| 144 |
+
time.sleep(1)
|
| 145 |
+
|
| 146 |
+
return results
|
| 147 |
+
|
| 148 |
+
except requests.exceptions.HTTPError as e:
|
| 149 |
+
if e.response.status_code == 403:
|
| 150 |
+
logger.warning("GitHub API rate limit ์ด๊ณผ")
|
| 151 |
+
elif e.response.status_code == 401:
|
| 152 |
+
logger.warning("GitHub API ์ธ์ฆ ์คํจ (ํ ํฐ์ด ์๊ฑฐ๋ ์๋ชป๋จ). ํ ํฐ ์์ด ๊ณ์ ์งํํฉ๋๋ค.")
|
| 153 |
+
else:
|
| 154 |
+
logger.error("GitHub ๊ฒ์ HTTP ์๋ฌ: %s", e, exc_info=True)
|
| 155 |
+
return []
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error("GitHub ๊ฒ์ ์คํจ: %s", e, exc_info=True)
|
| 158 |
+
return []
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def search_official_docs(query: str, limit: int = 3) -> List[SearchResult]:
|
| 162 |
+
"""Tavily API๋ฅผ ์ฌ์ฉํด ๊ณต์ ๋ฌธ์๋ฅผ ๊ฒ์ํ๋ค.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
query: ๊ฒ์ ์ฟผ๋ฆฌ
|
| 166 |
+
limit: ๋ฐํํ ์ต๋ ๊ฒฐ๊ณผ ์
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
SearchResult ๋ฆฌ์คํธ (์คํจ ์ ๋น ๋ฆฌ์คํธ)
|
| 170 |
+
"""
|
| 171 |
+
if not query.strip():
|
| 172 |
+
logger.warning("Official Docs ๊ฒ์: ๋น ์ฟผ๋ฆฌ")
|
| 173 |
+
return []
|
| 174 |
+
|
| 175 |
+
api_key = os.getenv("TAVILY_API_KEY", "").strip()
|
| 176 |
+
if not api_key:
|
| 177 |
+
logger.error("TAVILY_API_KEY ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ด ์์ง ์์ต๋๋ค.")
|
| 178 |
+
return []
|
| 179 |
+
|
| 180 |
+
try:
|
| 181 |
+
client = TavilyClient(api_key=api_key)
|
| 182 |
+
|
| 183 |
+
response = client.search(
|
| 184 |
+
query=query,
|
| 185 |
+
search_depth="basic",
|
| 186 |
+
max_results=limit,
|
| 187 |
+
include_domains=[
|
| 188 |
+
"docs.python.org",
|
| 189 |
+
"docs.oracle.com",
|
| 190 |
+
"spring.io/guides",
|
| 191 |
+
"developer.mozilla.org",
|
| 192 |
+
"reactjs.org/docs",
|
| 193 |
+
],
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
results = []
|
| 197 |
+
for item in response.get("results", []):
|
| 198 |
+
content = item.get("content", "")
|
| 199 |
+
url = item.get("url", "")
|
| 200 |
+
score = item.get("score", 0.5) # Tavily๊ฐ ์ ๊ณตํ๋ ๊ด๋ จ๋ ์ ์
|
| 201 |
+
|
| 202 |
+
results.append(
|
| 203 |
+
SearchResult(
|
| 204 |
+
source="Official Docs",
|
| 205 |
+
content=content,
|
| 206 |
+
url=url,
|
| 207 |
+
relevance_score=score,
|
| 208 |
+
)
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
logger.info("Tavily ๊ฒ์ ์ฑ๊ณต: %d๊ฐ ๊ฒฐ๊ณผ", len(results))
|
| 212 |
+
return results
|
| 213 |
+
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.error("Tavily ๊ฒ์ ์คํจ: %s", e, exc_info=True)
|
| 216 |
+
return []
|
| 217 |
+
|
CodeWeaver/src/utils/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""์ ํธ๋ฆฌํฐ ๋ชจ๋."""
|
| 2 |
+
|
| 3 |
+
from .tracing import ensure_tracing_enabled, trace_node
|
| 4 |
+
|
| 5 |
+
__all__ = ["ensure_tracing_enabled", "trace_node"]
|
| 6 |
+
|
| 7 |
+
|
CodeWeaver/src/utils/tracing.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangSmith ์ถ์ (tracing) ์ ํธ๋ฆฌํฐ ๋ชจ๋.
|
| 3 |
+
|
| 4 |
+
LangGraph ๋
ธ๋ ์คํ์ LangSmith์์ ์ถ์ ํ๊ณ ๋ชจ๋ํฐ๋งํ๊ธฐ ์ํ ๋๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 5 |
+
๊ณต์ ๋ฌธ์: https://docs.langchain.com/langsmith/trace-with-langgraph
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import logging
|
| 10 |
+
import asyncio
|
| 11 |
+
from functools import wraps
|
| 12 |
+
from typing import Any, Callable
|
| 13 |
+
from inspect import iscoroutinefunction
|
| 14 |
+
|
| 15 |
+
from langsmith import traceable
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def ensure_tracing_enabled() -> bool:
|
| 21 |
+
"""
|
| 22 |
+
LangSmith ์ถ์ ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์๋์ง ํ์ธํฉ๋๋ค.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
bool: ์ถ์ ์ด ํ์ฑํ๋์ด ์์ผ๋ฉด True, ๊ทธ๋ ์ง ์์ผ๋ฉด False
|
| 26 |
+
"""
|
| 27 |
+
required_vars = ["LANGCHAIN_TRACING_V2", "LANGCHAIN_API_KEY"]
|
| 28 |
+
|
| 29 |
+
missing_vars = [var for var in required_vars if not os.getenv(var)]
|
| 30 |
+
|
| 31 |
+
if missing_vars:
|
| 32 |
+
logger.warning(
|
| 33 |
+
"LangSmith ์ถ์ ์ด ๋นํ์ฑํ๋์์ต๋๋ค. ๋๋ฝ๋ ํ๊ฒฝ๋ณ์: %s",
|
| 34 |
+
", ".join(missing_vars)
|
| 35 |
+
)
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def trace_node(node_name: str) -> Callable:
|
| 42 |
+
"""
|
| 43 |
+
LangGraph ๋
ธ๋ ์คํ์ ์ถ์ ํ๋ ๋ฐ์ฝ๋ ์ดํฐ.
|
| 44 |
+
|
| 45 |
+
์ด ๋ฐ์ฝ๋ ์ดํฐ๋ ๊ฐ ๋
ธ๋์ ์
๋ ฅ/์ถ๋ ฅ, ์คํ ์๊ฐ, ์๋ฌ๋ฅผ
|
| 46 |
+
LangSmith ๋์๋ณด๋์ ์๋์ผ๋ก ๊ธฐ๋กํฉ๋๋ค.
|
| 47 |
+
๋๊ธฐ ๋ฐ ๋น๋๊ธฐ ํจ์ ๋ชจ๋ ์ง์ํฉ๋๋ค.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
node_name: LangSmith์ ํ์๋ ๋
ธ๋ ์ด๋ฆ
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Callable: ๋ฐ์ฝ๋ ์ดํธ๋ ํจ์
|
| 54 |
+
|
| 55 |
+
Example:
|
| 56 |
+
@trace_node("check_cache")
|
| 57 |
+
async def check_cache_node(state: AgentState) -> AgentState:
|
| 58 |
+
# ๋
ธ๋ ๋ก์ง
|
| 59 |
+
return state
|
| 60 |
+
"""
|
| 61 |
+
def decorator(func: Callable) -> Callable:
|
| 62 |
+
# async ํจ์์ธ์ง ํ์ธ
|
| 63 |
+
if iscoroutinefunction(func):
|
| 64 |
+
@wraps(func)
|
| 65 |
+
@traceable(name=node_name, run_type="chain")
|
| 66 |
+
async def async_wrapper(*args, **kwargs) -> Any:
|
| 67 |
+
try:
|
| 68 |
+
result = await func(*args, **kwargs)
|
| 69 |
+
return result
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error("๐ด ๋
ธ๋ ์คํจ: %s - %s", node_name, str(e))
|
| 72 |
+
raise
|
| 73 |
+
return async_wrapper
|
| 74 |
+
else:
|
| 75 |
+
@wraps(func)
|
| 76 |
+
@traceable(name=node_name, run_type="chain")
|
| 77 |
+
def sync_wrapper(*args, **kwargs) -> Any:
|
| 78 |
+
try:
|
| 79 |
+
result = func(*args, **kwargs)
|
| 80 |
+
return result
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error("๐ด ๋
ธ๋ ์คํจ: %s - %s", node_name, str(e))
|
| 83 |
+
raise
|
| 84 |
+
return sync_wrapper
|
| 85 |
+
return decorator
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ๋ชจ๋ import ์ ์๋์ผ๋ก ์ถ์ ์ค์ ํ์ธ
|
| 89 |
+
ensure_tracing_enabled()
|
| 90 |
+
|
| 91 |
+
|
CodeWeaver/src/vector_db/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .qdrant_client import QdrantManager
|
| 2 |
+
from .local_embeddings import LocalEmbeddingManager
|
| 3 |
+
|
| 4 |
+
__all__ = ["QdrantManager", "LocalEmbeddingManager"]
|
| 5 |
+
|
| 6 |
+
|
CodeWeaver/src/vector_db/local_embeddings.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
๋ก์ปฌ ์๋ฒ ๋ฉ ๊ด๋ฆฌ ๋ชจ๋.
|
| 3 |
+
|
| 4 |
+
BAAI/bge-m3 ๋ชจ๋ธ์ ์ฌ์ฉํด ๋ก์ปฌ์์ ์๋ฒ ๋ฉ์ ์์ฑํ๋ค.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
from typing import List
|
| 9 |
+
|
| 10 |
+
from sentence_transformers import SentenceTransformer
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class LocalEmbeddingManager:
|
| 16 |
+
"""BAAI/bge-m3 ๋ก์ปฌ ์๋ฒ ๋ฉ ์์ฑ๊ธฐ."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, model_name: str = "BAAI/bge-m3") -> None:
|
| 19 |
+
logger.info("๋ก์ปฌ ์๋ฒ ๋ฉ ๋ชจ๋ธ ๋ก๋ฉ ์ค: %s", model_name)
|
| 20 |
+
self.model = SentenceTransformer(model_name)
|
| 21 |
+
dim = self.model.get_sentence_embedding_dimension()
|
| 22 |
+
logger.info("๋ก์ปฌ ์๋ฒ ๋ฉ ๋ชจ๋ธ ๋ก๋ฉ ์๋ฃ (์ฐจ์: %d)", dim)
|
| 23 |
+
|
| 24 |
+
def get_embedding(self, text: str) -> List[float]:
|
| 25 |
+
"""๋จ์ผ ํ
์คํธ๋ฅผ ์๋ฒ ๋ฉ."""
|
| 26 |
+
embedding = self.model.encode(text, convert_to_numpy=True)
|
| 27 |
+
return embedding.tolist()
|
| 28 |
+
|
| 29 |
+
def get_embeddings_batch(self, texts: List[str]) -> List[List[float]]:
|
| 30 |
+
"""๋ฐฐ์น ํ
์คํธ ์๋ฒ ๋ฉ."""
|
| 31 |
+
embeddings = self.model.encode(texts, convert_to_numpy=True)
|
| 32 |
+
return embeddings.tolist()
|
| 33 |
+
|
| 34 |
+
|
CodeWeaver/src/vector_db/qdrant_client.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv # type: ignore[import]
|
| 7 |
+
from qdrant_client import QdrantClient, models
|
| 8 |
+
|
| 9 |
+
from src.vector_db.local_embeddings import LocalEmbeddingManager
|
| 10 |
+
|
| 11 |
+
# .env ํ์ผ์์ ํ๊ฒฝ ๋ณ์ ๋ก๋ (๋ก์ปฌ ๊ฐ๋ฐ ํธ์์ฑ)
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class QdrantManager:
|
| 18 |
+
"""Qdrant Cloud ๊ธฐ๋ฐ ๋ฒกํฐ ์บ์ ๊ด๋ฆฌ ํด๋์ค.
|
| 19 |
+
|
| 20 |
+
- ์๋ฒ ๋ฉ ์์ฑ: ๋ก์ปฌ BAAI/bge-m3
|
| 21 |
+
- ๋ฒกํฐ ์ ์ฅ/๊ฒ์: Qdrant Cloud
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, collection_name: str = "CodeWeaver") -> None:
|
| 25 |
+
"""Qdrant Cloud ํด๋ผ์ด์ธํธ๋ฅผ ์ด๊ธฐํํ๊ณ ์ปฌ๋ ์
์ ์ค๋นํ๋ค."""
|
| 26 |
+
qdrant_url = os.getenv("QDRANT_URL", "").strip()
|
| 27 |
+
qdrant_api_key = os.getenv("QDRANT_API_KEY", "").strip()
|
| 28 |
+
|
| 29 |
+
if not qdrant_url or not qdrant_api_key:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
"QDRANT_URL ๋ฐ QDRANT_API_KEY ํ๊ฒฝ ๋ณ์๊ฐ ๋ชจ๋ ์ค์ ๋์ด ์์ด์ผ ํฉ๋๋ค."
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Qdrant Cloud ๊ณต์ ๊ฐ์ด๋์ ์ ์ฌํ ์ด๊ธฐํ ํํ ์ฌ์ฉ
|
| 35 |
+
# https://qdrant.tech/documentation/tutorials-and-examples/cloud-inference-hybrid-search/
|
| 36 |
+
self.client = QdrantClient(
|
| 37 |
+
url=qdrant_url,
|
| 38 |
+
api_key=qdrant_api_key,
|
| 39 |
+
timeout=30,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
self.collection_name = collection_name
|
| 43 |
+
self.embedding_manager = LocalEmbeddingManager()
|
| 44 |
+
|
| 45 |
+
logger.info("QdrantManager ์ด๊ธฐํ: collection=%s, url=%s", collection_name, qdrant_url)
|
| 46 |
+
|
| 47 |
+
# ์ปฌ๋ ์
์ด ์๋ค๋ฉด ์์ฑ
|
| 48 |
+
self._init_collection()
|
| 49 |
+
|
| 50 |
+
def _init_collection(self) -> None:
|
| 51 |
+
"""์ปฌ๋ ์
์ด ์์ผ๋ฉด ์์ฑํ๋ค."""
|
| 52 |
+
try:
|
| 53 |
+
exists = self.client.collection_exists(self.collection_name)
|
| 54 |
+
except Exception as e: # pragma: no cover - ๋ฐฉ์ด์ ์ฝ๋
|
| 55 |
+
logger.error("Qdrant ์ปฌ๋ ์
์กด์ฌ ์ฌ๋ถ ํ์ธ ์คํจ: %s", e, exc_info=True)
|
| 56 |
+
raise
|
| 57 |
+
|
| 58 |
+
if exists:
|
| 59 |
+
logger.info("Qdrant ์ปฌ๋ ์
์ด๋ฏธ ์กด์ฌ: %s", self.collection_name)
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
self.client.create_collection(
|
| 64 |
+
collection_name=self.collection_name,
|
| 65 |
+
vectors_config=models.VectorParams(
|
| 66 |
+
size=1024, # bge-m3 ์๋ฒ ๋ฉ ์ฐจ์
|
| 67 |
+
distance=models.Distance.COSINE,
|
| 68 |
+
),
|
| 69 |
+
)
|
| 70 |
+
logger.info("Qdrant ์ปฌ๋ ์
์์ฑ ์๋ฃ: %s", self.collection_name)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error("Qdrant ์ปฌ๋ ์
์์ฑ ์คํจ: %s", e, exc_info=True)
|
| 73 |
+
raise
|
| 74 |
+
|
| 75 |
+
async def get_embedding(self, text: str) -> List[float]:
|
| 76 |
+
"""๋ก์ปฌ ์๋ฒ ๋ฉ ๋ชจ๋ธ์ ์ฌ์ฉํด ํ
์คํธ ์๋ฒ ๋ฉ์ ์์ฑํ๋ค."""
|
| 77 |
+
try:
|
| 78 |
+
embedding = self.embedding_manager.get_embedding(text)
|
| 79 |
+
logger.debug("์๋ฒ ๋ฉ ์์ฑ ์๋ฃ (๊ธธ์ด=%d)", len(embedding))
|
| 80 |
+
return embedding
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error("์๋ฒ ๋ฉ ์์ฑ ์คํจ: %s", e, exc_info=True)
|
| 83 |
+
raise
|
| 84 |
+
|
| 85 |
+
async def search_cache(
|
| 86 |
+
self,
|
| 87 |
+
question: str,
|
| 88 |
+
threshold: float = 0.85,
|
| 89 |
+
) -> Optional[str]:
|
| 90 |
+
"""์ง๋ฌธ์ ๋ํ ์บ์๋ ๋ต๋ณ์ Qdrant์์ ๊ฒ์ํ๋ค.
|
| 91 |
+
|
| 92 |
+
threshold๋ณด๋ค ๋์ score๋ฅผ ๊ฐ์ง ๊ฒฐ๊ณผ๊ฐ ์์ ๋๋ง answer๋ฅผ ๋ฐํํ๋ค.
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
embedding = await self.get_embedding(question)
|
| 96 |
+
except Exception:
|
| 97 |
+
# ์ด๋ฏธ get_embedding ๋ด๋ถ์์ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ฏ๋ก ์ฌ๊ธฐ์๋ ์กฐ์ฉํ ์คํจ ์ฒ๋ฆฌ
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
# Qdrant ๊ณต์ ๋ฌธ์: query_points๋ฅผ ์ฌ์ฉํ ๋ฒกํฐ ๊ฒ์
|
| 102 |
+
# ๋จ์ผ ๋ฒกํฐ ์ปฌ๋ ์
์ ๊ฒฝ์ฐ query ํ๋ผ๋ฏธํฐ์ ๋ฒกํฐ ๋ฆฌ์คํธ๋ฅผ ์ง์ ์ ๋ฌ
|
| 103 |
+
# https://qdrant.tech/documentation/tutorials-and-examples/cloud-inference-hybrid-search/
|
| 104 |
+
results = self.client.query_points(
|
| 105 |
+
collection_name=self.collection_name,
|
| 106 |
+
query=embedding, # ๋จ์ผ ๋ฒกํฐ ์ปฌ๋ ์
: ๋ฒกํฐ๋ฅผ ์ง์ ์ ๋ฌ
|
| 107 |
+
limit=1,
|
| 108 |
+
with_payload=True,
|
| 109 |
+
)
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logger.error("Qdrant ์บ์ ๊ฒ์ ์คํจ: %s", e, exc_info=True)
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
if not results.points:
|
| 115 |
+
logger.info("์บ์ ๋ฏธ์ค: ๊ฒฐ๊ณผ ์์ (question=%s)", question)
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
top = results.points[0]
|
| 119 |
+
score = getattr(top, "score", None)
|
| 120 |
+
payload = getattr(top, "payload", {}) or {}
|
| 121 |
+
|
| 122 |
+
if score is None:
|
| 123 |
+
logger.warning("๊ฒ์ ๊ฒฐ๊ณผ์ score๊ฐ ์์ต๋๋ค. payload=%s", payload)
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
+
if score < threshold:
|
| 127 |
+
logger.info(
|
| 128 |
+
"์บ์ ๋ฏธ์ค: score(%.4f) < threshold(%.4f) (question=%s)",
|
| 129 |
+
score,
|
| 130 |
+
threshold,
|
| 131 |
+
question,
|
| 132 |
+
)
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
answer = payload.get("answer")
|
| 136 |
+
if answer is None:
|
| 137 |
+
logger.info("์บ์ ํํธ์ด์ง๋ง payload์ answer๊ฐ ์์ต๋๋ค. payload=%s", payload)
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
logger.info(
|
| 141 |
+
"์บ์ ํํธ: score=%.4f, question=%s, answer_length=%d",
|
| 142 |
+
score,
|
| 143 |
+
question,
|
| 144 |
+
len(str(answer)),
|
| 145 |
+
)
|
| 146 |
+
return str(answer)
|
| 147 |
+
|
| 148 |
+
async def save_to_cache(self, question: str, answer: str) -> None:
|
| 149 |
+
"""์ง๋ฌธ-๋ต๋ณ ์์ Qdrant ์บ์์ ์ ์ฅํ๋ค.
|
| 150 |
+
|
| 151 |
+
๋์ผํ ์ง๋ฌธ์ ๋ํด์๋ deterministic ID๋ฅผ ์ฌ์ฉํ์ฌ,
|
| 152 |
+
upsert ์ ๊ธฐ์กด ์ํธ๋ฆฌ๋ฅผ ๋ฎ์ด์ฐ๊ฒ ํจ์ผ๋ก์จ ์ค๋ณต์ ๋ฐฉ์งํ๋ค.
|
| 153 |
+
"""
|
| 154 |
+
try:
|
| 155 |
+
embedding = await self.get_embedding(question)
|
| 156 |
+
except Exception:
|
| 157 |
+
# ์๋ฒ ๋ฉ ์คํจ ์ ์บ์์ ์ ์ฅํ์ง ์๋๋ค.
|
| 158 |
+
logger.warning("์๋ฒ ๋ฉ ์คํจ๋ก ์ธํด ์บ์์ ์ ์ฅํ์ง ์์. question=%s", question)
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
# UUID ๋์ ์ง๋ฌธ ํด์ ๊ธฐ๋ฐ deterministic ID ์ฌ์ฉ
|
| 162 |
+
# โ ๋์ผ ์ง๋ฌธ = ๋์ผ ID โ upsert๊ฐ ๋ฎ์ด์ฐ๊ธฐ๋ก ๋์ โ ์ค๋ณต ๋ฐฉ์ง
|
| 163 |
+
#
|
| 164 |
+
# ์ฃผ์: Qdrant point id๋ "unsigned int" ๋๋ "UUID"๋ง ํ์ฉํ๋ค.
|
| 165 |
+
# ๋ฐ๋ผ์ sha256 hex(64์)๋ฅผ ๊ทธ๋๋ก ์ฐ์ง ์๊ณ , ์ 32์๋ฅผ UUID ํฌ๋งท์ผ๋ก ๋ณํํด ์ฌ์ฉํ๋ค.
|
| 166 |
+
digest = hashlib.sha256(question.encode("utf-8")).hexdigest()
|
| 167 |
+
point_id = f"{digest[:8]}-{digest[8:12]}-{digest[12:16]}-{digest[16:20]}-{digest[20:32]}"
|
| 168 |
+
|
| 169 |
+
# ๊ธฐ์กด ์ํธ๋ฆฌ ์กด์ฌ ์(๋ฎ์ด์ฐ๊ธฐ) ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค. ์คํจํด๋ upsert๋ ๊ณ์ ์๋.
|
| 170 |
+
try:
|
| 171 |
+
existing = self.client.retrieve(
|
| 172 |
+
collection_name=self.collection_name,
|
| 173 |
+
ids=[point_id],
|
| 174 |
+
with_payload=False,
|
| 175 |
+
with_vectors=False,
|
| 176 |
+
)
|
| 177 |
+
if existing:
|
| 178 |
+
logger.info("๊ธฐ์กด ์บ์ ์ํธ๋ฆฌ๋ฅผ ๋ฎ์ด์๋๋ค: point_id=%s", point_id)
|
| 179 |
+
except Exception:
|
| 180 |
+
pass
|
| 181 |
+
|
| 182 |
+
point = models.PointStruct(
|
| 183 |
+
id=point_id,
|
| 184 |
+
vector=embedding,
|
| 185 |
+
payload={
|
| 186 |
+
"question": question,
|
| 187 |
+
"answer": answer,
|
| 188 |
+
},
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
self.client.upsert(
|
| 193 |
+
collection_name=self.collection_name,
|
| 194 |
+
points=[point],
|
| 195 |
+
)
|
| 196 |
+
logger.info(
|
| 197 |
+
"Qdrant ์บ์์ ์ ์ฅ ์๋ฃ (hash ID๋ก ์ค๋ณต ๋ฐฉ์ง): point_id=%s, question_length=%d, answer_length=%d",
|
| 198 |
+
point_id,
|
| 199 |
+
len(question),
|
| 200 |
+
len(answer),
|
| 201 |
+
)
|
| 202 |
+
except Exception as e:
|
| 203 |
+
logger.error("Qdrant ์บ์ ์ ์ฅ ์คํจ: %s", e, exc_info=True)
|
| 204 |
+
|
| 205 |
+
async def get_cache_stats(self) -> Dict[str, int]:
|
| 206 |
+
"""ํ์ฌ ์ปฌ๋ ์
์ ์บ์ ํต๊ณ๋ฅผ ๋ฐํํ๋ค."""
|
| 207 |
+
try:
|
| 208 |
+
info = self.client.get_collection(self.collection_name)
|
| 209 |
+
# qdrant_client์ CollectionInfo๋ points_count ์์ฑ์ ์ ๊ณต
|
| 210 |
+
points_count = getattr(info, "points_count", 0) or 0
|
| 211 |
+
logger.debug(
|
| 212 |
+
"Qdrant ์บ์ ํต๊ณ ์กฐํ: collection=%s, total_entries=%d",
|
| 213 |
+
self.collection_name,
|
| 214 |
+
points_count,
|
| 215 |
+
)
|
| 216 |
+
return {"total_entries": int(points_count)}
|
| 217 |
+
except Exception as e:
|
| 218 |
+
logger.error("Qdrant ์บ์ ํต๊ณ ์กฐํ ์คํจ: %s", e, exc_info=True)
|
| 219 |
+
# ํธ์ถ ์ธก์์ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ฐธ๊ณ ํ ์ ์๋๋ก ํฌํจ
|
| 220 |
+
return {
|
| 221 |
+
"total_entries": 0,
|
| 222 |
+
"error": str(e), # type: ignore[dict-item]
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
|
CodeWeaver/test_result.txt
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
๏ปฟ============================================================
|
| 2 |
+
Phase 5: ?์ํๆดน๋ช์??็ฑั๋ฅ?์ข์ญ
ๆดัโ ๅฏย๏ง?
|
| 3 |
+
============================================================
|
| 4 |
+
??graph.py ๆดัะฆ ๅฏย๏ง??๊น๋ฌ
|
| 5 |
+
|
| 6 |
+
[?๊พฉ๋ ?โฅ๋ ๅฏย๏ง?
|
| 7 |
+
??build_search_subgraph
|
| 8 |
+
??build_single_question_subgraph
|
| 9 |
+
??route_after_plan
|
| 10 |
+
??build_agent_graph
|
| 11 |
+
??create_agent
|
| 12 |
+
|
| 13 |
+
[?์๊ต
???โฅ๋ ๅฏย๏ง?
|
| 14 |
+
??route_after_generate - ?๋บค๊ธฝ ?์๊ต
??
|
| 15 |
+
|
| 16 |
+
[Import ๅฏย๏ง?
|
| 17 |
+
??initiate_dynamic_search_node - import ?์๊ต
??
|
| 18 |
+
??fanout_multi_questions - import ?์๊ต
??
|
| 19 |
+
??run_single_question_worker_node - import ?์๊ต
??
|
| 20 |
+
??collect_subgraph_result_node - import ็ฐ๋ถฝ???
|
| 21 |
+
|
| 22 |
+
[๏ง๋ถฟ์ค ๆดน๋ช์???๋ช๋ฑถ ๅฏย๏ง?
|
| 23 |
+
??create_plan
|
| 24 |
+
??handle_too_many_questions
|
| 25 |
+
??combine_answers
|
| 26 |
+
??collect_subgraph_result
|
| 27 |
+
??single_question_subgraph
|
| 28 |
+
|
| 29 |
+
============================================================
|
| 30 |
+
nodes.py ๆดัโ ๅฏย๏ง?
|
| 31 |
+
============================================================
|
| 32 |
+
??nodes.py ๆดัะฆ ๅฏย๏ง??๊น๋ฌ
|
| 33 |
+
|
| 34 |
+
[?์๊ต
???โฅ๋ ๅฏย๏ง?
|
| 35 |
+
??_build_search_subgraph_local - ?๋บค๊ธฝ ?์๊ต
??
|
| 36 |
+
??_get_single_question_agent - ?๋บค๊ธฝ ?์๊ต
??
|
| 37 |
+
??run_single_question_worker_node - ?๋บค๊ธฝ ?์๊ต
??
|
| 38 |
+
??initiate_dynamic_search_node - ?๋บค๊ธฝ ?์๊ต
??
|
| 39 |
+
??fanout_multi_questions - ?๋บค๊ธฝ ?์๊ต
??
|
| 40 |
+
|
| 41 |
+
[็ฐ๋ถฝ????โฅ๋ ๅฏย๏ง?
|
| 42 |
+
??collect_subgraph_result_node
|
| 43 |
+
|
| 44 |
+
============================================================
|
| 45 |
+
ๅฏย๏ง?ๅฏ๊ณ๋ต ?๋ถฟ๋น
|
| 46 |
+
============================================================
|
| 47 |
+
???๊น๋ฌ: graph.py ๆดัโ
|
| 48 |
+
???๊น๋ฌ: nodes.py ๆดัโ
|
| 49 |
+
|
| 50 |
+
?๋ฆ ๏งโค๋ฑบ ๅฏย๏ง??๋ฆ๋ต! ็ฑั๋ฅ?์ข์ญ
???๊น๋ฌ?๊ณธ์ๆฟก??๊พจ์ฆบ?์๋ฟ?๋ฌ๋ฒ??
|
| 51 |
+
|
| 52 |
+
[ๅช์๊ฝ ?ั๋น]
|
| 53 |
+
???โฅ์ช ๏ง๋ะฆ ?๋ฏ์ ?๊พจ์ช?๋ช์ฃ ?ั๊ถ??ๅชย?ฮฝ๋ธณ ?์ํๆดน๋ช์?๊พจ์ค ็ฐ๋ถฟํ
ง
|
| 54 |
+
??้บย๏ง?ๆดน๋ช์?๊พจ๋ ๆจ๊พช์ท/้บ๊พง๋ฆฐ/่น๋ฌ๋น๏ง??๋๋ฆ (orchestration)
|
| 55 |
+
??่น๋ญ์??worker ?๋ช๋ฑถ ่ซ?ไปฅ๋ฌ๋ฌ ๆดน๋ช์??้ฎ๋ฎ๋ ?์๊ต
(300+ ไปฅ?
|
| 56 |
+
??ๆดัโ ๏ง๋์?? ้บย๏ง?orchestration) vs ?๋จฏ๋(processing)
|
CodeWeaver/ui/app.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import uuid
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# ํ๊ฒฝ ๋ณ์ ๋ก๋ (์์ด์ ํธ/ํธ๋ ์ด์ฑ import ์ด์ ์ ์คํ)
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
# ํ๋ก์ ํธ ๋ฃจํธ๋ฅผ ๊ฒฝ๋ก์ ์ถ๊ฐ
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
+
|
| 17 |
+
from src.agent.graph import agent
|
| 18 |
+
from src.agent.state import AgentState
|
| 19 |
+
|
| 20 |
+
# ๋ก๊น
์ค์ (WARNING ์ด์๋ง ์ถ๋ ฅ)
|
| 21 |
+
logging.basicConfig(
|
| 22 |
+
level=logging.WARNING,
|
| 23 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ก๊ทธ๋ WARNING๋ง
|
| 27 |
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 28 |
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
| 29 |
+
logging.getLogger("langsmith").setLevel(logging.WARNING)
|
| 30 |
+
|
| 31 |
+
# CodeWeaver ๋ชจ๋ ๋ก๊ทธ๋ WARNING๋ง (๋ก๊ทธ ๋นํ์ฑํ)
|
| 32 |
+
logging.getLogger("src.agent").setLevel(logging.WARNING)
|
| 33 |
+
logging.getLogger("src.tools").setLevel(logging.WARNING)
|
| 34 |
+
logging.getLogger("src.vector_db").setLevel(logging.WARNING)
|
| 35 |
+
|
| 36 |
+
logger = logging.getLogger(__name__)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
async def chat(
|
| 40 |
+
message: str,
|
| 41 |
+
history: list,
|
| 42 |
+
thread_id: str,
|
| 43 |
+
) -> str:
|
| 44 |
+
"""
|
| 45 |
+
์ฌ์ฉ์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๊ณ ์์ด์ ํธ ์๋ต์ ๋ฐํํฉ๋๋ค.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
message: ์ฌ์ฉ์ ์
๋ ฅ ๋ฉ์์ง
|
| 49 |
+
history: ๋ํ ๋ด์ญ (Gradio ์๋ ๊ด๋ฆฌ)
|
| 50 |
+
thread_id: ์ธ์
๋ณ ๊ณ ์ ID (MemorySaver๊ฐ ๋ํ ๋งฅ๋ฝ ์ถ์ ์ ์ฌ์ฉ)
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
์์ด์ ํธ์ ์ต์ข
๋ต๋ณ
|
| 54 |
+
"""
|
| 55 |
+
if not message or not message.strip():
|
| 56 |
+
return "์ง๋ฌธ์ ์
๋ ฅํด์ฃผ์ธ์."
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
# ์ด๊ธฐ ์ํ ์์ฑ (Pydantic BaseModel ์ฌ์ฉ)
|
| 60 |
+
from langchain_core.messages import HumanMessage
|
| 61 |
+
|
| 62 |
+
initial_state = AgentState(
|
| 63 |
+
user_question=message.strip(),
|
| 64 |
+
messages=[HumanMessage(content=message.strip())],
|
| 65 |
+
conversation_history=history[-5:] if history else None, # ์ต๊ทผ 5ํด๋ง ์ ๋ฌ
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# ์ธ์
๋ณ thread_id๋ฅผ config์ ์ ๋ฌ (MemorySaver๊ฐ ๋ํ ๋งฅ๋ฝ ์ ์ง)
|
| 69 |
+
config = {"configurable": {"thread_id": thread_id}}
|
| 70 |
+
|
| 71 |
+
# ์์ด์ ํธ ์คํ
|
| 72 |
+
result = await agent.ainvoke(initial_state, config=config)
|
| 73 |
+
|
| 74 |
+
# ์ต์ข
๋ต๋ณ ์ถ์ถ
|
| 75 |
+
final_answer = result.get("final_answer", "๋ต๋ณ์ ์์ฑํ์ง ๋ชปํ์ต๋๋ค.")
|
| 76 |
+
|
| 77 |
+
return final_answer
|
| 78 |
+
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error("์๋ฌ ๋ฐ์: %s", e, exc_info=True)
|
| 81 |
+
return f"โ ๏ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}\n๋ค์ ์๋ํด์ฃผ์ธ์."
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def create_demo() -> gr.Blocks:
|
| 85 |
+
"""Gradio ์ธํฐํ์ด์ค๋ฅผ ์์ฑํฉ๋๋ค."""
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# CSS ์คํ์ผ (๊น๋ํ ๋์์ธ)
|
| 90 |
+
# - Gradio ๊ธฐ๋ณธ CSS๊ฐ .contain/.gradio-container ํญ์ ๋ฎ์ด์ฐ๋ ๊ฒฝ์ฐ๊ฐ ์์ด
|
| 91 |
+
# ๋ ๋ค !important๋ก ๊ณ ์ ํ์ฌ "์ฒ์๋ถํฐ ๋์ ํญ"์ ํ์คํ ์ ์งํฉ๋๋ค.
|
| 92 |
+
css = """
|
| 93 |
+
.gradio-container {
|
| 94 |
+
max-width: 1280px !important;
|
| 95 |
+
width: min(1280px, 100%) !important;
|
| 96 |
+
margin: 0 auto !important;
|
| 97 |
+
}
|
| 98 |
+
.contain {
|
| 99 |
+
max-width: 1280px !important;
|
| 100 |
+
width: min(1280px, 100%) !important;
|
| 101 |
+
margin: 0 auto !important;
|
| 102 |
+
padding-top: 1.5rem;
|
| 103 |
+
}
|
| 104 |
+
.message { font-size: 1.1rem; line-height: 1.6; }
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
with gr.Blocks(
|
| 108 |
+
title="CodeWeaver - AI ๊ฐ๋ฐ ๋์ฐ๋ฏธ",
|
| 109 |
+
theme=gr.themes.Soft(),
|
| 110 |
+
css=css
|
| 111 |
+
) as demo:
|
| 112 |
+
|
| 113 |
+
gr.Markdown("""
|
| 114 |
+
# ๐ค CodeWeaver
|
| 115 |
+
### AI ๊ธฐ๋ฐ ๊ฐ๋ฐ ์ง๋ฌธ ๋ต๋ณ ์์คํ
|
| 116 |
+
|
| 117 |
+
์ด๋ณด ๊ฐ๋ฐ์๋ฅผ ์ํ ์น์ ํ AI ๋์ฐ๋ฏธ์
๋๋ค.
|
| 118 |
+
|
| 119 |
+
**์ฃผ์ ๊ธฐ๋ฅ:**
|
| 120 |
+
- โ
์๋ฌ ํด๊ฒฐ (๋๋ฒ๊น
)
|
| 121 |
+
- โ
๊ฐ๋
ํ์ต
|
| 122 |
+
- โ
์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ ๊ฐ์ ์ ์
|
| 123 |
+
- โ
**๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ** (์ต๋ 2๊ฐ๊น์ง ๋์ ์ฒ๋ฆฌ)
|
| 124 |
+
- โ
**๋ํ ๋งฅ๋ฝ ์ดํด** (์ด์ ๋ํ๋ฅผ ์ฐธ๊ณ ํ ํ์ ์ง๋ฌธ ๋ต๋ณ)
|
| 125 |
+
- โ
**์ค๋งํธ ์บ์ฑ** (์ ์ฌ ์ง๋ฌธ ์ฆ์ ๋ต๋ณ)
|
| 126 |
+
- โ
**์๋ ๊ฒ์ ๊ฐ์ ** (๊ฒฐ๊ณผ ๋ถ์กฑ ์ ์ฟผ๋ฆฌ ์๋ ์ต์ ํ)
|
| 127 |
+
|
| 128 |
+
๐ฌ ๊ฐ๋ฐ ๊ด๋ จ ์ง๋ฌธ์ ์์ ๋กญ๊ฒ ํด๋ณด์ธ์!
|
| 129 |
+
- ๋จ์ผ ์ง๋ฌธ: "Spring Boot JPA N+1 ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์?"
|
| 130 |
+
- ๋ค์ค ์ง๋ฌธ: "JWT๊ฐ ๋ญ์ผ? CORS๋?" (์ต๋ 2๊ฐ)
|
| 131 |
+
- ํ์ ์ง๋ฌธ: "์ข ๋ ์ฝ๊ฒ ์ค๋ช
ํด์ค" (์ด์ ๋ต๋ณ ์ฐธ๊ณ )
|
| 132 |
+
""")
|
| 133 |
+
|
| 134 |
+
# ์ธ์
๋ณ ๊ณ ์ ID (๋ธ๋ผ์ฐ์ ์ธ์
๋ง๋ค ๋
๋ฆฝ์ ์ผ๋ก ์์ฑ)
|
| 135 |
+
session_id = gr.State(value=lambda: str(uuid.uuid4()))
|
| 136 |
+
|
| 137 |
+
# ์ฑํ
์ธํฐํ์ด์ค
|
| 138 |
+
chatbot_interface = gr.ChatInterface(
|
| 139 |
+
fn=chat,
|
| 140 |
+
examples=None, # examples๋ ์๋ Accordion์์ ์๋ ์ฒ๋ฆฌ
|
| 141 |
+
chatbot=gr.Chatbot(height=500),
|
| 142 |
+
textbox=gr.Textbox(
|
| 143 |
+
placeholder="์ง๋ฌธ์ ์
๋ ฅํ์ธ์...",
|
| 144 |
+
container=False,
|
| 145 |
+
scale=7
|
| 146 |
+
),
|
| 147 |
+
retry_btn=None,
|
| 148 |
+
undo_btn=None,
|
| 149 |
+
clear_btn="๐๏ธ ๋ํ ์ด๊ธฐํ",
|
| 150 |
+
additional_inputs=[session_id], # thread_id ์ ๋ฌ
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# Clear ๋ฒํผ ํด๋ฆญ ์ ์ ์ธ์
ID ์์ฑ (์ ๋ํ ์์)
|
| 154 |
+
def reset_session():
|
| 155 |
+
new_id = str(uuid.uuid4())
|
| 156 |
+
return new_id
|
| 157 |
+
|
| 158 |
+
# Clear ๋ฒํผ์ ์ธ์
๋ฆฌ์
ํธ๋ค๋ฌ ์ถ๊ฐ
|
| 159 |
+
if chatbot_interface.clear_btn:
|
| 160 |
+
chatbot_interface.clear_btn.click(
|
| 161 |
+
reset_session,
|
| 162 |
+
None,
|
| 163 |
+
session_id,
|
| 164 |
+
queue=False
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# ๋น ๋ฅธ ์ง๋ฌธ ๋ฒํผ๋ค (Accordion ๋ฐ์ผ๋ก ๋ถ๋ฆฌ)
|
| 168 |
+
gr.Markdown("### ๐ฌ ์์ ์ง๋ฌธ")
|
| 169 |
+
example_questions = [
|
| 170 |
+
"Spring Boot JPA N+1 ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์?",
|
| 171 |
+
"ImportError: No module named 'requests' ํด๊ฒฐ ๋ฐฉ๋ฒ",
|
| 172 |
+
"Docker Compose ์ค์ ์์ ๋ฅผ ์๋ ค์ฃผ์ธ์",
|
| 173 |
+
"์ด ์ฝ๋๋ฅผ ๊ฐ์ ํด์ฃผ์ธ์: for i in range(len(arr)): print(arr[i])",
|
| 174 |
+
"JWT๊ฐ ๋ญ์ผ? CORS๋?", # ๋ค์ค ์ง๋ฌธ ์์
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
with gr.Row():
|
| 178 |
+
for question in example_questions:
|
| 179 |
+
btn = gr.Button(
|
| 180 |
+
question,
|
| 181 |
+
variant="secondary",
|
| 182 |
+
size="sm",
|
| 183 |
+
scale=1,
|
| 184 |
+
)
|
| 185 |
+
# ๋ฒํผ ํด๋ฆญ ์ ์
๋ ฅ์ฐฝ์ ์๋ ์
๋ ฅ
|
| 186 |
+
btn.click(
|
| 187 |
+
fn=lambda q=question: q,
|
| 188 |
+
outputs=[chatbot_interface.textbox],
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# ์ ๋ณด ์น์
|
| 192 |
+
with gr.Accordion("๐ ์์คํ
์ ๋ณด", open=False):
|
| 193 |
+
gr.Markdown("""
|
| 194 |
+
### ์ฌ์ฉ๋ ๊ธฐ์
|
| 195 |
+
- **LLM**: Gemini 2.5 Flash Lite
|
| 196 |
+
- **์๋ฒ ๋ฉ**: BAAI/bge-m3 (๋ก์ปฌ)
|
| 197 |
+
- **๋ฒกํฐ DB**: Qdrant Cloud
|
| 198 |
+
- **๊ฒ์ API**: Stack Overflow, GitHub, Tavily
|
| 199 |
+
- **ํ๋ ์์ํฌ**: LangGraph
|
| 200 |
+
|
| 201 |
+
### ์ฃผ์ ๊ธฐ๋ฅ
|
| 202 |
+
- ๐ **๋ณ๋ ฌ ๊ฒ์**: Stack Overflow, GitHub, ๊ณต์ ๋ฌธ์ ๋์ ๊ฒ์
|
| 203 |
+
- ๐พ **์๋ฏธ์ ์บ์ฑ**: ์ ์ฌ ์ง๋ฌธ(์๊ณ๊ฐ 0.85 ์ด์) ์ฆ์ ๋ต๋ณ
|
| 204 |
+
- ๐ฏ **์๋ ๊ธฐ๋ฐ ๋ผ์ฐํ
**: debugging/learning/code_review ์๋ ๋ถ๋ฅ
|
| 205 |
+
- ๐ **์๋ ์ฟผ๋ฆฌ ๊ฐ์ **: ๊ฒ์ ๊ฒฐ๊ณผ ๋ถ์กฑ ์ ์ต๋ 1ํ ์๋ ์ต์ ํ
|
| 206 |
+
- ๐ **์ด๋ณด์ ์นํ ๋ต๋ณ**: ์๋๋ณ ๋ง์ถคํ ๋ต๋ณ ๊ตฌ์กฐ
|
| 207 |
+
- ๐ **๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ**: ๋
๋ฆฝ ์ง๋ฌธ 2๊ฐ๊น์ง ๋ณ๋ ฌ ์ฒ๋ฆฌ
|
| 208 |
+
- ๐ฌ **๋ํ ๋งฅ๋ฝ ์ดํด**: clarification ์ง๋ฌธ์ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ
|
| 209 |
+
|
| 210 |
+
### LangGraph๋ก ๊ตฌํํ ํต์ฌ ๊ธฐ๋ฅ
|
| 211 |
+
1. โ
**Conditional Edges**: ์ง๋ฌธ ์ ํ/์บ์ ์ฌ๋ถ/๊ฒ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋์ ๋ผ์ฐํ
|
| 212 |
+
2. โ
**Send API**: 3๊ฐ ๊ฒ์ ์์ค ๋ณ๋ ฌ ์คํ (fan-out/fan-in)
|
| 213 |
+
3. โ
**Subgraph**: ๊ฒ์ ๊ฒฐ๊ณผ ํํฐ๋ง ๋ฐ ์์ฝ ํ์ดํ๋ผ์ธ
|
| 214 |
+
4. โ
**Map-Reduce**: ๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ ์ ๊ฐ ์ง๋ฌธ๋ณ ๋
๋ฆฝ ์คํ ํ ๊ฒฐ๊ณผ ํตํฉ
|
| 215 |
+
5. โ
**Checkpointing**: MemorySaver๋ก ๋ํ ์ํ ์ ์ฅ ๋ฐ ์ฌ๊ฐ
|
| 216 |
+
6. โ
**Pydantic Typed State**: ํ์
์์ ํ ์ํ ๊ด๋ฆฌ
|
| 217 |
+
|
| 218 |
+
### GitHub
|
| 219 |
+
[ํ๋ก์ ํธ ์์ค์ฝ๋](https://github.com/shin-heewon/codeweaver)
|
| 220 |
+
""")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# ์ฌ์ฉ ๊ฐ์ด๋
|
| 225 |
+
with gr.Accordion("๐ก ์ฌ์ฉ ํ", open=False):
|
| 226 |
+
gr.Markdown("""
|
| 227 |
+
### 1. ๊ตฌ์ฒด์ ์ผ๋ก ์ง๋ฌธํ๊ธฐ
|
| 228 |
+
- โ "ํ์ด์ฌ ์๋ฌ"
|
| 229 |
+
- โ
"ImportError: No module named 'requests' ํด๊ฒฐ ๋ฐฉ๋ฒ"
|
| 230 |
+
|
| 231 |
+
### 2. ์ง๋ฌธ ์ ํ๋ณ ์์
|
| 232 |
+
- **๋๋ฒ๊น
**: "์ด ์๋ฌ ๋ฉ์์ง๋ ๋ฌด์์ ์๋ฏธํ๋์?"
|
| 233 |
+
- **ํ์ต**: "JPA N+1 ๋ฌธ์ ๋ ์ ๋ฐ์ํ๋์?"
|
| 234 |
+
- **์ฝ๋ ๋ฆฌ๋ทฐ**: "์ด ์ฝ๋๋ฅผ ๋ ํจ์จ์ ์ผ๋ก ๊ฐ์ ํ๋ ค๋ฉด?"
|
| 235 |
+
|
| 236 |
+
### 3. ๋ค์ค ์ง๋ฌธ ์ฌ์ฉ๋ฒ
|
| 237 |
+
- โ
**2๊ฐ๊น์ง ๊ฐ๋ฅ**: "JWT๊ฐ ๋ญ์ผ? CORS๋?"
|
| 238 |
+
- โ **3๊ฐ ์ด์ ๋ถ๊ฐ**: "JWT? CORS? Docker?" โ ์๋ด ๋ฉ์์ง ํ์
|
| 239 |
+
- ๐ก **ํ**: ๊ด๋ จ ์ง๋ฌธ์ ํ๋๋ก ํตํฉํ๊ฑฐ๋, ์์ฐจ์ ์ผ๋ก ์ง๋ฌธํ์ธ์
|
| 240 |
+
|
| 241 |
+
### 4. ๋ํ ๋งฅ๋ฝ ํ์ฉ
|
| 242 |
+
- **ํ์ ์ง๋ฌธ**: "์ข ๋ ์ฝ๊ฒ ์ค๋ช
ํด์ค", "์์ ์ฝ๋๋ก ๋ณด์ฌ์ค"
|
| 243 |
+
- **์ ๊ฐ๋
์ง๋ฌธ**: ๋ํ ์ค์๋ "Event Listener๋ ๋ญ์ผ?" ๊ฐ์ ๋
๋ฆฝ ์ง๋ฌธ ๊ฐ๋ฅ
|
| 244 |
+
- ๐ก **ํ**: ์ด์ ๋ํ๋ฅผ ์ฐธ๊ณ ํ ๋ต๋ณ์ด ํ์ํ๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์ง๋ฌธํ์ธ์
|
| 245 |
+
|
| 246 |
+
### 5. ์๋ต ์๊ฐ
|
| 247 |
+
- **์ฒซ ์ง๋ฌธ**: 20~30์ด ์์ (๊ฒ์ + ๋ต๋ณ ์์ฑ)
|
| 248 |
+
- **์ ์ฌ ์ง๋ฌธ**: ์ฆ์ ๋ต๋ณ (์บ์ ํ์ฉ, ์๊ณ๊ฐ 0.85 ์ด์)
|
| 249 |
+
- **๋ค์ค ์ง๋ฌธ**: ๊ฐ ์ง๋ฌธ๋ณ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ํจ์จ์
|
| 250 |
+
|
| 251 |
+
### 6. ๋ ๋์ ๋ต๋ณ์ ์ํ ํ
|
| 252 |
+
- ์๋ฌ ๋ฉ์์ง๋ฅผ ํฌํจํด์ฃผ์ธ์
|
| 253 |
+
- ์ฌ์ฉ ์ค์ธ ์ธ์ด/ํ๋ ์์ํฌ๋ฅผ ๋ช
์ํ์ธ์
|
| 254 |
+
- ์๋ํ๋ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ํจ๊ป ์๋ ค์ฃผ์ธ์
|
| 255 |
+
- ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํ๋ฉด ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํฉ๋๋ค (์ต๋ 1ํ)
|
| 256 |
+
""")
|
| 257 |
+
|
| 258 |
+
return demo
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# ์ฑ ์์ฑ
|
| 262 |
+
app = create_demo()
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
# ๋ก์ปฌ ์คํ
|
| 267 |
+
app.launch(
|
| 268 |
+
server_name="0.0.0.0",
|
| 269 |
+
server_port=7860,
|
| 270 |
+
share=False, # True๋ก ํ๋ฉด ๊ณต๊ฐ URL ์์ฑ
|
| 271 |
+
show_api=False, # Gradio 4.44.x ๋ฒ๊ทธ ์ฐํ์ฉ
|
| 272 |
+
)
|