ใ……ใ…Žใ…‡ commited on
Commit
9803acf
ยท
1 Parent(s): d4a4cca

Update app.py: Change logging level from INFO to WARNING

Browse files
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
+ )