ใ……ใ…Žใ…‡ commited on
Commit
515f392
ยท
1 Parent(s): 4f3be99

Add CodeWeaver Gradio app

Browse files
ARCHITECTURE.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CodeWeaver ์•„ํ‚คํ…์ฒ˜ (์‹ค์ œ ์ฝ”๋“œ ๊ธฐ์ค€)
2
+
3
+ ์ด ๋ฌธ์„œ๋Š” ํ˜„์žฌ ์ €์žฅ์†Œ์˜ CodeWeaver๊ฐ€ **์–ด๋–ค ์ˆœ์„œ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€**, ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์›๋ฆฌ๊ฐ€ ๋ฌด์—‡์ธ์ง€(์ƒํƒœ/๋ผ์šฐํŒ…/๋ณ‘๋ ฌํ™”/์บ์‹œ)๋ฅผ **์ฝ”๋“œ์™€ 1:1๋กœ ์ •ํ•ฉ**๋˜๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.
4
+
5
+ ## ์ „์ฒด ๊ตฌ์„ฑ ์š”์†Œ
6
+
7
+ - **UI**: Gradio ์ฑ„ํŒ… UI (`CodeWeaver/ui/app.py`)
8
+ - ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ `AgentState`๋กœ ํฌ์žฅํ•œ ๋’ค `agent.ainvoke(..., config={"configurable": {"thread_id": ...}})`๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
9
+ - **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜(๊ทธ๋ž˜ํ”„)**: LangGraph `StateGraph` (`CodeWeaver/src/agent/graph.py`)
10
+ - `START โ†’ create_plan`๋กœ ์ง„์ž… ํ›„, ์งˆ๋ฌธ ์œ ํ˜•/๊ฐœ์ˆ˜์— ๋”ฐ๋ผ ๋ถ„๊ธฐํ•ฉ๋‹ˆ๋‹ค.
11
+ - ์ฒดํฌํฌ์ธํŒ…: `MemorySaver` ์‚ฌ์šฉ(์Šค๋ ˆ๋“œ/์„ธ์…˜ ๋‹จ์œ„ ์ƒํƒœ ์œ ์ง€).
12
+ - **๋…ธ๋“œ ๊ตฌํ˜„**: (`CodeWeaver/src/agent/nodes.py`)
13
+ - ์งˆ๋ฌธ ๋ถ„์„, ์บ์‹œ ์กฐํšŒ, ์˜๋„ ๋ถ„๋ฅ˜, 3์†Œ์Šค ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰, ๊ฒฐ๊ณผ ํ‰๊ฐ€/๋ฆฌํŒŒ์ธ, ํ•„ํ„ฐ๋ง/์š”์•ฝ, ๋‹ต๋ณ€ ์ƒ์„ฑ, ๋‹ค์ค‘ ์งˆ๋ฌธ ๊ฒฐํ•ฉ ๋“ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
14
+ - **์ƒํƒœ ๋ชจ๋ธ(Reducer ํฌํ•จ)**: (`CodeWeaver/src/agent/state.py`)
15
+ - `search_results`๋Š” `Annotated[List[SearchResult], add]`๋กœ **๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ž๋™ ๋ณ‘ํ•ฉ**๋ฉ๋‹ˆ๋‹ค.
16
+ - `intermediate_steps`, `multi_answers`๋Š” **๋ฆฌ์…‹ ํ† ํฐ์„ ์ง€์›ํ•˜๋Š” ์ปค์Šคํ…€ reducer**๋กœ, ์ฒดํฌํฌ์ธํŒ…/์Šค๋ ˆ๋“œ ์œ ์ง€ ์‹œ ์ด์ „ ํ„ด์˜ ๋ˆ„์ ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
17
+ - **์บ์‹œ(Vector DB)**: Qdrant Cloud (`CodeWeaver/src/vector_db/qdrant_client.py`)
18
+ - ์ž„๋ฒ ๋”ฉ์€ ๋กœ์ปฌ `BAAI/bge-m3`(`sentence-transformers`)๋กœ ์ƒ์„ฑ, Qdrant์— ์ €์žฅ/๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
19
+ - **๊ฒ€์ƒ‰ ์†Œ์Šค**: (`CodeWeaver/src/tools/search_tools.py`)
20
+ - Stack Overflow(๊ณต์‹ StackExchange API), GitHub Code Search API, Tavily(๊ณต์‹๋ฌธ์„œ ๋„๋ฉ”์ธ ์ œํ•œ) ์‚ฌ์šฉ.
21
+
22
+ ## ์‚ฌ์šฉ์ž ์ œ๊ณต ๊ทธ๋ž˜ํ”„์™€์˜ ์ •ํ•ฉ์„ฑ
23
+
24
+ ์‚ฌ์šฉ์ž๊ป˜์„œ ์ œ๊ณตํ•œ Mermaid ๊ทธ๋ž˜ํ”„๋Š” ์ด ํ”„๋กœ์ ํŠธ์˜ ์˜๋„์™€ **๋Œ€๋ถ€๋ถ„ ์ผ์น˜**ํ•ฉ๋‹ˆ๋‹ค.
25
+
26
+ ### ์ผ์น˜ํ•˜๋Š” ๋ถ€๋ถ„(ํ•ต์‹ฌ ํŒŒ์ดํ”„๋ผ์ธ)
27
+
28
+ - `create_plan`์—์„œ **single_topic / multiple_questions(2๊ฐœ) / too_many(3+)** ๋ถ„๊ธฐ
29
+ - ๋‹จ์ผ ์งˆ๋ฌธ(ํ˜น์€ ๋‹จ์ผ ์ฃผ์ œ)์—์„œ:
30
+ - `analyze_question โ†’ check_cache โ†’ (hit๋ฉด return_cached_answer) / (miss๋ฉด classify_intent)`
31
+ - `classify_intent` ์ดํ›„ 3์†Œ์Šค ๊ฒ€์ƒ‰์„ Send API๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰(fan-out)ํ•˜๊ณ  `collect_results`์—์„œ fan-in
32
+ - `evaluate_results โ†’ (ํ•„์š” ์‹œ refine_search 1ํšŒ) โ†’ filter_and_score โ†’ summarize_results โ†’ generate_answer`
33
+ - `evaluate_results`๊ฐ€ ๋ถ€์กฑํ•˜๋ฉด `refine_search โ†’ classify_intent`๋กœ **์ตœ๋Œ€ 1ํšŒ ๋ฃจํ”„**
34
+
35
+ ### ์‹ค์ œ ์ฝ”๋“œ์—์„œ ์ถ”๊ฐ€/๋ณ€ํ˜•๋œ ๋ถ€๋ถ„(์ค‘์š”)
36
+
37
+ 1) **clarification(๋ณด์ถฉ ์š”์ฒญ) ์ „์šฉ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌ**
38
+
39
+ - `analyze_question` ๊ฒฐ๊ณผ๊ฐ€ `clarification`์ด๋ฉด
40
+ - **์บ์‹œ/๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ **
41
+ - `generate_with_history`๋กœ ๋ฐ”๋กœ ๋‹ต๋ณ€ํ•˜๊ณ  ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
42
+
43
+ 2) **multiple_questions fan-out์€ `analyze_question`๋กœ ์ง์ ‘ ๋“ค์–ด๊ฐ€์ง€ ์•Š์Œ**
44
+
45
+ ์‚ฌ์šฉ์ž ๊ทธ๋ž˜ํ”„๋Š” โ€œdynamic์—์„œ Send๋กœ analyze_question์„ 2๋ฒˆ ํ˜ธ์ถœโ€ ํ˜•ํƒœ์— ๊ฐ€๊น์ง€๋งŒ, ์‹ค์ œ ๊ตฌํ˜„์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.
46
+
47
+ - ์‹ค์ œ ๊ตฌํ˜„์€ `fanout_multi_questions`๊ฐ€ `Send("run_single_question_worker", child_state)`๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
48
+ - ์ด์œ : outer graph์—์„œ ์งˆ๋ฌธ 2๊ฐœ๋ฅผ ๋™์‹œ์— ๋™์ผ ํŒŒ์ดํ”„๋ผ์ธ(analyze/cache/intent/โ€ฆ)์œผ๋กœ ๋Œ๋ฆฌ๋ฉด
49
+ - `question_type`, `cached_result` ๊ฐ™์€ **scalar ์ฑ„๋„(state ํ•„๋“œ)**์ด ๋ณ‘๋ ฌ ์—…๋ฐ์ดํŠธ ์ถฉ๋Œ์„ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
50
+ - ๋”ฐ๋ผ์„œ **worker ๋‚ด๋ถ€์—์„œ ๋ณ„๋„์˜ โ€˜๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„โ€™๋ฅผ ์‹คํ–‰**ํ•˜๊ณ ,
51
+ - outer graph์—๋Š” reducer ์ฑ„๋„์ธ `multi_answers`๋งŒ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ถฉ๋Œ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
52
+
53
+ ## ์‹ค์ œ ์‹คํ–‰ ํ๋ฆ„(์ฝ”๋“œ ๊ธฐ์ค€)
54
+
55
+ ### 1) UI โ†’ Agent ์‹คํ–‰(์—”ํŠธ๋ฆฌ)
56
+
57
+ `CodeWeaver/ui/app.py`์—์„œ:
58
+
59
+ - ์ž…๋ ฅ ๋ฌธ์ž์—ด `message`๋ฅผ `AgentState(user_question=..., messages=[HumanMessage(...)], ...)`๋กœ ๋งŒ๋“ค๊ณ 
60
+ - `thread_id`๋ฅผ `config={"configurable":{"thread_id": thread_id}}`๋กœ ์ „๋‹ฌํ•˜์—ฌ `agent.ainvoke()` ์‹คํ–‰
61
+ - `MemorySaver`๊ฐ€ `thread_id` ๋‹จ์œ„๋กœ ์ƒํƒœ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
62
+
63
+ ### 2) ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„(Top-level) ํ๋ฆ„
64
+
65
+ `CodeWeaver/src/agent/graph.py` ๊ธฐ์ค€ ๋ฉ”์ธ ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
66
+
67
+ ```mermaid
68
+ graph TD
69
+ startNode[START] --> createPlan[create_plan]
70
+
71
+ createPlan -->|single_topic| analyzeQuestion[analyze_question]
72
+ createPlan -->|multiple_questions_2| initiateDynamic[initiate_dynamic_search]
73
+ createPlan -->|too_many_3plus| tooMany[handle_too_many_questions]
74
+
75
+ tooMany --> endNode[END]
76
+
77
+ analyzeQuestion -->|clarification| withHistory[generate_with_history]
78
+ withHistory --> endNode
79
+
80
+ analyzeQuestion -->|new_topic_or_independent| checkCache[check_cache]
81
+ checkCache -->|hit| returnCached[return_cached_answer]
82
+ returnCached --> endNode
83
+
84
+ checkCache -->|miss| classifyIntent[classify_intent]
85
+
86
+ classifyIntent --> searchSO[search_stackoverflow]
87
+ classifyIntent --> searchGH[search_github]
88
+ classifyIntent --> searchDocs[search_official_docs]
89
+
90
+ searchSO --> collect[collect_results]
91
+ searchGH --> collect
92
+ searchDocs --> collect
93
+
94
+ collect --> evalNode[evaluate_results]
95
+ evalNode -->|needs_refinement_and_lt1| refine[refine_search]
96
+ refine --> classifyIntent
97
+
98
+ evalNode -->|sufficient_or_ge1| searchSubgraph[search_subgraph]
99
+ searchSubgraph --> generateAnswer[generate_answer]
100
+ generateAnswer --> routeAfterGen[route_after_generate]
101
+ routeAfterGen -->|single| endNode
102
+ routeAfterGen -->|multi| combine[combine_answers]
103
+ combine --> endNode
104
+
105
+ initiateDynamic --> fanout[fanout_multi_questions]
106
+ fanout --> worker[run_single_question_worker]
107
+ worker --> combine
108
+ ```
109
+
110
+ ### 3) `create_plan`: ์งˆ๋ฌธ ๊ฐœ์ˆ˜/ํ˜•ํƒœ ํŒ๋ณ„ + โ€œ3๊ฐœ ์ด์ƒโ€ ํ•˜๋“œ ๊ฐ€๋“œ
111
+
112
+ `create_plan_node`๋Š” ์ž…๋ ฅ์„ ์•„๋ž˜ 3๊ฐ€์ง€๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.
113
+
114
+ - **single_topic**: ํ•˜๋‚˜์˜ ์ฃผ์ œ๋ฅผ ๋‹ค์–‘ํ•œ ๊ด€์ ์œผ๋กœ ๋ฌป๋Š” ํ˜•ํƒœ
115
+ - **multiple_questions**: ๋…๋ฆฝ ์งˆ๋ฌธ 2๊ฐœ
116
+ - **too_many**: ๋…๋ฆฝ ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ
117
+
118
+ ์ถ”๊ฐ€๋กœ, LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋‹ค์Œ ์กฐ๊ฑด์ด๋ฉด **๊ฒฐ์ •๋ก ์ ์œผ๋กœ too_many**๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
119
+
120
+ - ๋ฌผ์Œํ‘œ๊ฐ€ 3๊ฐœ ์ด์ƒ
121
+ - ๋˜๋Š” โ€œ์งˆ๋ฌธ ํ›„๋ณดโ€๊ฐ€ 3๊ฐœ ์ด์ƒ(์ค„๋ฐ”๊ฟˆ/๋ฒˆํ˜ธ/๊ตฌ๋ถ„์ž ๋“ฑ์œผ๋กœ ์ถ”์ •)
122
+
123
+ ๋˜ํ•œ ์ฒดํฌํฌ์ธํŒ… ์ƒํƒœ ๋ˆ„์ ์„ ๋ง‰๊ธฐ ์œ„ํ•ด, ๋งค ์‹คํ–‰ ์‹œ์ž‘ ์‹œ `multi_answers`๋ฅผ ๋ฆฌ์…‹ ํ† ํฐ์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
124
+
125
+ ### 4) `analyze_question`: ์งˆ๋ฌธ ํƒ€์ž…(clarification/new_topic/independent) + ์บ์‹œ ์ ๊ฒฉ์„ฑ ํŒ๋‹จ
126
+
127
+ `analyze_question_node`๊ฐ€ LLM์œผ๋กœ ์•„๋ž˜ ๊ฐ’์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
128
+
129
+ - `question_type`: `clarification | new_topic | independent`
130
+ - `should_cache`: ์บ์‹œ ์ €์žฅ ์—ฌ๋ถ€
131
+ - `canonical_question`: ์บ์‹œ์šฉ ์ •๊ทœํ™” ์งˆ๋ฌธ(should_cache=true์ผ ๋•Œ)
132
+
133
+ ๋ผ์šฐํŒ…์€ `graph.py`์˜ `route_after_analysis`์—์„œ:
134
+
135
+ - `clarification` โ†’ `generate_with_history` (๊ฒ€์ƒ‰/์บ์‹œ ์ƒ๋žต)
136
+ - ๋‚˜๋จธ์ง€ โ†’ `check_cache`
137
+
138
+ ### 5) ์บ์‹œ(`check_cache` / `return_cached_answer`)
139
+
140
+ `check_cache_node`๋Š” Qdrant์—์„œ ์œ ์‚ฌ ์งˆ๋ฌธ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
141
+
142
+ - ์ž„๋ฒ ๋”ฉ: ๋กœ์ปฌ `BAAI/bge-m3` (1024์ฐจ์›)
143
+ - ์ž„๊ณ„๊ฐ’: cosine score **0.85 ์ด์ƒ**์ด๋ฉด hit๋กœ ๊ฐ„์ฃผ
144
+
145
+ hit๋ฉด `return_cached_answer_node`๊ฐ€ ์ €์žฅ๋œ ๋‹ต๋ณ€์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
146
+
147
+ ### 6) ์˜๋„ ๋ถ„๋ฅ˜(`classify_intent`)
148
+
149
+ `classify_intent_node`๊ฐ€ ์งˆ๋ฌธ์„ `debugging | learning | code_review`๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.
150
+
151
+ ์ด ๊ฐ’์€ ๊ฒ€์ƒ‰ ๊ฐœ์ˆ˜ ๋“ฑ ์ผ๋ถ€ ์ •์ฑ…์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค(์˜ˆ: StackOverflow๋Š” debugging์ด๋ฉด ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ด).
152
+
153
+ ### 7) ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰(fan-out) โ†’ ์ˆ˜์ง‘(fan-in)
154
+
155
+ `classify_intent` ์ดํ›„ conditional edge ํ•จ์ˆ˜๊ฐ€ `Send(...)` 3๊ฐœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
156
+
157
+ - `search_stackoverflow_node`
158
+ - `search_github_node`
159
+ - `search_official_docs_node`
160
+
161
+ ๊ฐ ๋…ธ๋“œ๋Š” `{"search_results": [..]}`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , `AgentState.search_results`์˜ reducer(`add`)๊ฐ€ ์ด๋ฅผ ์ž๋™ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
162
+
163
+ `collect_results_node`๋Š” ๋ณ‘ํ•ฉ๋œ ์ด ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜๋งŒ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
164
+
165
+ ### 8) ๊ฒฐ๊ณผ ํ‰๊ฐ€(`evaluate_results`)์™€ ์ฟผ๋ฆฌ ๋ฆฌํŒŒ์ธ(`refine_search`)
166
+
167
+ `evaluate_results_node`๋Š” ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ โ€œ๊ฐœ์„  ํ•„์š”โ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
168
+
169
+ - ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ < 2 โ†’ ๊ฐœ์„  ํ•„์š”
170
+ - (relevance_score๊ฐ€ ์žˆ๋‹ค๋ฉด) ํ‰๊ท  ์ ์ˆ˜ < 0.5 โ†’ ๊ฐœ์„  ํ•„์š”
171
+
172
+ `refine_search_node`๋Š” LLM์ด `MORE_SPECIFIC | MORE_GENERAL | TRANSLATE` ์ „๋žต์„ ์„ ํƒํ•ด ์ฟผ๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
173
+
174
+ - ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€: `refinement_count < 1`์ผ ๋•Œ๋งŒ 1ํšŒ ํ—ˆ์šฉ
175
+ - ์žฌ๊ฒ€์ƒ‰์„ ์œ„ํ•ด `search_results`๋ฅผ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  `classify_intent`๋กœ ๋˜๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
176
+
177
+ ### 9) `search_subgraph`: ํ•„ํ„ฐ๋ง + ์š”์•ฝ
178
+
179
+ ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„์—๋Š” `search_subgraph`๊ฐ€ โ€œํ•˜๋‚˜์˜ ๋…ธ๋“œโ€์ฒ˜๋Ÿผ ๋ถ™์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
180
+
181
+ - `filter_and_score`: ์ตœ์†Œ ๊ธธ์ด/URL ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ ํ›„, ์ƒ์œ„ ์ผ๋ถ€์— ๋Œ€ํ•ด ๊ด€๋ จ๋„ ์ ์ˆ˜ ๋ถ€์—ฌ
182
+ - `summarize_results`: ๊ฐ ๊ฒฐ๊ณผ๋ฅผ 2~3๋ฌธ์žฅ์œผ๋กœ ์š”์•ฝ
183
+
184
+ ### 10) `generate_answer`: ๋‹ต๋ณ€ ์ƒ์„ฑ + (์กฐ๊ฑด๋ถ€) ์บ์‹œ ์ €์žฅ
185
+
186
+ `generate_answer_node`๋Š” ์˜๋„์— ๋”ฐ๋ผ ํ…œํ”Œ๋ฆฟ์„ ๋ฐ”๊ฟ” ์ตœ์ข… ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ ์บ์‹œ ์ €์žฅ ์ •์ฑ…:
189
+
190
+ - `question_type`๊ฐ€ `new_topic` ๋˜๋Š” `independent`์ด๊ณ  `should_cache`๊ฐ€ true์ด๋ฉด ์ €์žฅ
191
+ - `clarification`์€ ์ €์žฅํ•˜์ง€ ์•Š์Œ(๋ผ์šฐํŒ…์ƒ ๋ณดํ†ต ์—ฌ๊ธฐ๋กœ ์˜ค์ง€ ์•Š์ง€๋งŒ ๋ฐฉ์–ด์ ์œผ๋กœ ์ฒดํฌ)
192
+
193
+ ### 11) ๋‹ค์ค‘ ์งˆ๋ฌธ(multiple_questions) ์ฒ˜๋ฆฌ ์›๋ฆฌ
194
+
195
+ ๋‹ค์ค‘ ์งˆ๋ฌธ์˜ ํ•ต์‹ฌ์€ โ€œouter graph๋Š” ์ถฉ๋Œ ์—†์ด orchestration๋งŒ, ์‹ค์ œ ํŒŒ์ดํ”„๋ผ์ธ์€ worker ๋‚ด๋ถ€์—์„œ ์‹คํ–‰โ€์ž…๋‹ˆ๋‹ค.
196
+
197
+ #### ํ๋ฆ„
198
+
199
+ - `create_plan(case=multiple_questions)` โ†’ `initiate_dynamic_search` (์ค€๋น„)
200
+ - `fanout_multi_questions`(conditional edge)์ด ์งˆ๋ฌธ 2๊ฐœ๋ฅผ ๊ฐ๊ฐ `run_single_question_worker`๋กœ Send
201
+ - `run_single_question_worker_node` ๋‚ด๋ถ€์—์„œ **๋‹จ์ผ ์งˆ๋ฌธ์šฉ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ณ„๋„ compile/์‹คํ–‰**
202
+ - worker ๊ฒฐ๊ณผ๋Š” `multi_answers`์— append(reducer๋กœ ๋ณ‘ํ•ฉ)
203
+ - ๋ชจ๋“  worker๊ฐ€ ๋๋‚˜๋ฉด `combine_answers_node`๊ฐ€ Markdown์œผ๋กœ ๊ฒฐํ•ฉ
204
+
205
+ #### ์™œ worker๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?
206
+
207
+ outer graph์—์„œ ๋™์ผํ•œ state๋ฅผ ๋ณต์ œํ•ด `analyze_question`๋ถ€ํ„ฐ ๋™์‹œ์— ๋Œ๋ฆฌ๋ฉด,
208
+ scalar ์ฑ„๋„(`question_type`, `cached_result` ๋“ฑ)์ด ์„œ๋กœ ๋ฎ์–ด์“ฐ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
209
+
210
+ ๊ทธ๋ž˜์„œ ์‹ค์ œ ๊ตฌํ˜„์€:
211
+
212
+ - worker ๋‚ด๋ถ€์—์„œ ๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„๋ฅผ ๋Œ๋ฆฌ๊ณ 
213
+ - outer state์—๋Š” **reducer ์ฑ„๋„์ธ `multi_answers`๋งŒ** ์—…๋ฐ์ดํŠธ
214
+
215
+ ์ด ๋ฐฉ์‹์œผ๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.
216
+
217
+ ## ํ™˜๊ฒฝ ๋ณ€์ˆ˜(์‹คํ–‰์— ํ•„์š”ํ•œ ์‹ค์ œ ๊ฐ’)
218
+
219
+ ํ•„์ˆ˜:
220
+
221
+ - `GOOGLE_API_KEY`: Gemini ํ˜ธ์ถœ(`langchain-google-genai`)
222
+ - `QDRANT_URL`, `QDRANT_API_KEY`: Qdrant Cloud ์บ์‹œ
223
+ - `TAVILY_API_KEY`: ๊ณต์‹ ๋ฌธ์„œ ๊ฒ€์ƒ‰(Tavily)
224
+
225
+ ์„ ํƒ:
226
+
227
+ - `GITHUB_TOKEN`: GitHub API rate limit ์™„ํ™”(์—†์œผ๋ฉด 60 req/hr ์ˆ˜์ค€)
228
+ - `LANGCHAIN_TRACING_V2`, `LANGCHAIN_API_KEY`: LangSmith ํŠธ๋ ˆ์ด์‹ฑ(์„ ํƒ)
229
+
230
+
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/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,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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
+ initiate_dynamic_search_node,
39
+ combine_answers_node,
40
+ fanout_multi_questions,
41
+ run_single_question_worker_node,
42
+ )
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ def build_search_subgraph() -> StateGraph:
48
+ """
49
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
50
+
51
+ ํ๋ฆ„: filter_and_score โ†’ summarize_results
52
+
53
+ ์ด ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„์—์„œ ํ•˜๋‚˜์˜ ๋…ธ๋“œ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋ฉฐ,
54
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ํ•„ํ„ฐ๋ง๊ณผ ์š”์•ฝ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
55
+
56
+ Returns:
57
+ ์ปดํŒŒ์ผ๋œ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„
58
+ """
59
+ # ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ (AgentState ์‚ฌ์šฉ)
60
+ subgraph = StateGraph(AgentState)
61
+
62
+ # ๋…ธ๋“œ ์ถ”๊ฐ€
63
+ subgraph.add_node("filter_and_score", filter_and_score_node)
64
+ subgraph.add_node("summarize_results", summarize_results_node)
65
+
66
+ # ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ๋‚ด๋ถ€ ํ๋ฆ„ ์ •์˜
67
+ # START โ†’ filter_and_score โ†’ summarize_results โ†’ END
68
+ subgraph.add_edge(START, "filter_and_score")
69
+ subgraph.add_edge("filter_and_score", "summarize_results")
70
+ subgraph.add_edge("summarize_results", END)
71
+
72
+ return subgraph.compile()
73
+
74
+
75
+ def route_after_analysis(state: AgentState) -> Literal["generate_with_history", "check_cache"]:
76
+ """
77
+ ์งˆ๋ฌธ ๋ถ„์„ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
78
+
79
+ Phase 2: New Routing Structure
80
+
81
+ Args:
82
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
83
+
84
+ Returns:
85
+ - "generate_with_history": ํ›„์† ์งˆ๋ฌธ โ†’ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€
86
+ - "check_cache": ๋…๋ฆฝ ์งˆ๋ฌธ โ†’ ์บ์‹œ ํ™•์ธ
87
+ """
88
+ # NOTE: ๊ณผ๊ฑฐ ์ฒดํฌํฌ์ธํŠธ/๊ตฌ๋ฒ„์ „ ์ƒํƒœ๊ฐ’ ํ˜ธํ™˜์„ ์œ„ํ•ด ๊ตฌ๊ฐ’๋„ ๋งคํ•‘ ์ฒ˜๋ฆฌ
89
+ raw_qtype = state.question_type or "independent"
90
+ legacy_map = {
91
+ "followup": "clarification",
92
+ "cache_candidate": "independent",
93
+ "new_search": "independent",
94
+ }
95
+ question_type = legacy_map.get(raw_qtype, raw_qtype)
96
+
97
+ if question_type == "clarification":
98
+ return "generate_with_history"
99
+
100
+ # new_topic / independent ๋Š” ๋ชจ๋‘ ์บ์‹œ ํ™•์ธ(ํžˆํŠธ๋ฉด ๊ฒ€์ƒ‰ ์ƒ๋žต, ๋ฏธ์Šค๋ฉด ๊ฒ€์ƒ‰)
101
+ return "check_cache"
102
+
103
+
104
+ def route_after_plan(state: AgentState) -> Literal["analyze_question", "initiate_dynamic_search", "handle_too_many_questions"]:
105
+ """
106
+ create_plan ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
107
+
108
+ Phase 4: Dynamic Parallel Search
109
+
110
+ Args:
111
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
112
+
113
+ Returns:
114
+ - "analyze_question": ๋‹จ์ผ ์ฃผ์ œ โ†’ ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ ์‹คํ–‰
115
+ - "initiate_dynamic_search": ๋‹ค์ค‘ ์งˆ๋ฌธ (2๊ฐœ) โ†’ Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ์‹คํ–‰
116
+ - "handle_too_many_questions": ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ โ†’ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
117
+ """
118
+ plan = state.plan or {}
119
+ case = plan.get("case", "single_topic")
120
+
121
+ if case == "too_many":
122
+ return "handle_too_many_questions"
123
+ elif case == "multiple_questions":
124
+ return "initiate_dynamic_search"
125
+ else:
126
+ return "analyze_question"
127
+
128
+
129
+ def route_after_cache(state: AgentState) -> Literal["return_cached_answer", "classify_intent"]:
130
+ """
131
+ ์บ์‹œ ํžˆํŠธ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
132
+
133
+ Phase 3 โ†’ Phase 4: create_plan ์ œ๊ฑฐ๋จ (์ด๋ฏธ START์—์„œ ์‹คํ–‰)
134
+
135
+ Args:
136
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
137
+
138
+ Returns:
139
+ - "return_cached_answer": ์บ์‹œ ํžˆํŠธ ์‹œ ์ฆ‰์‹œ ๋‹ต๋ณ€ ๋ฐ˜ํ™˜
140
+ - "classify_intent": ์บ์‹œ ๋ฏธ์Šค ์‹œ ์˜๋„ ๋ถ„๋ฅ˜
141
+ """
142
+ if state.cached_result:
143
+ return "return_cached_answer"
144
+ else:
145
+ return "classify_intent"
146
+
147
+
148
+ def route_after_generate(state: AgentState) -> Literal["combine_answers", END]:
149
+ """
150
+ generate_answer ํ›„ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
151
+
152
+ Phase 4: Dynamic Parallel Search
153
+
154
+ Args:
155
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
156
+
157
+ Returns:
158
+ - "combine_answers": ๋‹ค์ค‘ ์งˆ๋ฌธ โ†’ ๋‹ต๋ณ€ ํ†ตํ•ฉ
159
+ - END: ๋‹จ์ผ ์งˆ๋ฌธ โ†’ ์ข…๋ฃŒ
160
+ """
161
+ if state.is_multi_question:
162
+ return "combine_answers"
163
+ return END
164
+
165
+
166
+ def route_after_evaluation(state: AgentState) -> Literal["refine_search", "search_subgraph"]:
167
+ """
168
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‰๊ฐ€ ํ›„ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
169
+
170
+ Phase 3: Open Deep Research ํŒจํ„ด - ์ฟผ๋ฆฌ ๊ฐœ์„  ๋ฃจํ”„
171
+
172
+ Args:
173
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
174
+
175
+ Returns:
176
+ - "refine_search": ๊ฒฐ๊ณผ ๋ถ€์กฑ & ๊ฐœ์„  ํšŸ์ˆ˜ 0ํšŒ โ†’ ์ฟผ๋ฆฌ ๊ฐœ์„ 
177
+ - "search_subgraph": ๊ฒฐ๊ณผ ์ถฉ๋ถ„ or ๊ฐœ์„  ํšŸ์ˆ˜ 1ํšŒ โ†’ ํ•„ํ„ฐ๋ง ์ง„ํ–‰
178
+ """
179
+ needs_refinement = state.needs_refinement
180
+ refinement_count = state.refinement_count
181
+
182
+ # ์•ˆ์ „์žฅ์น˜: ์ตœ๋Œ€ 1ํšŒ๋งŒ ๊ฐœ์„ 
183
+ if needs_refinement and refinement_count < 1:
184
+ return "refine_search"
185
+ else:
186
+ return "search_subgraph"
187
+
188
+
189
+ def initiate_parallel_search(state: AgentState):
190
+ """
191
+ Send API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 3๊ฐœ์˜ ๊ฒ€์ƒ‰ ๋…ธ๋“œ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
192
+
193
+ LangGraph Send API (Map-Reduce ํŒจํ„ด):
194
+ - ๊ฐ ๊ฒ€์ƒ‰ ๋…ธ๋“œ์— ๋™์ผํ•œ state๋ฅผ ์ „์†ก
195
+ - ๋ชจ๋“  ๋…ธ๋“œ๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋จ
196
+ - ๊ฒฐ๊ณผ๋Š” ์ž๋™์œผ๋กœ ๋จธ์ง€๋จ
197
+
198
+ Args:
199
+ state: ํ˜„์žฌ ์—์ด์ „ํŠธ ์ƒํƒœ
200
+
201
+ Returns:
202
+ Send ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ (fan-out)
203
+ """
204
+
205
+ # Send API๋ฅผ ์‚ฌ์šฉํ•œ fan-out
206
+ # 3๊ฐœ์˜ ๊ฒ€์ƒ‰ ๋…ธ๋“œ๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋จ
207
+ return [
208
+ Send("search_stackoverflow", state),
209
+ Send("search_github", state),
210
+ Send("search_official_docs", state),
211
+ ]
212
+
213
+
214
+ def build_agent_graph() -> StateGraph:
215
+ """
216
+ CodeWeaver ์—์ด์ „ํŠธ์˜ ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
217
+
218
+ Phase 4: Dynamic Parallel Search for Multiple Questions
219
+
220
+ ์ „์ฒด ํ๋ฆ„:
221
+ 1. START โ†’ create_plan (์งˆ๋ฌธ ์œ ํ˜• ๋ฐ ๊ฐœ์ˆ˜ ํŒ๋‹จ)
222
+ 2. ์งˆ๋ฌธ ์œ ํ˜•์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ:
223
+ - single_topic: analyze_question โ†’ ๊ธฐ์กด ๊ทธ๋ž˜ํ”„
224
+ - multiple_questions: initiate_dynamic_search โ†’ Send API (๊ฐ ์งˆ๋ฌธ๋งˆ๋‹ค ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ ๋…๋ฆฝ ์‹คํ–‰)
225
+ - too_many: handle_too_many_questions โ†’ END
226
+ 3. analyze_question โ†’ ์งˆ๋ฌธ ๋ถ„์„
227
+ - clarification: generate_with_history โ†’ END
228
+ - new_topic/independent: check_cache
229
+ 4. ์บ์‹œ ํ™•์ธ:
230
+ - ํžˆํŠธ: return_cached_answer โ†’ END
231
+ - ๋ฏธ์Šค: classify_intent
232
+ 5. Send API (๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ fan-out):
233
+ - classify_intent โ†’ 3๊ฐœ ๊ฒ€์ƒ‰ ๋…ธ๋“œ ๋ณ‘๋ ฌ ์‹คํ–‰
234
+ 6. collect_results (fan-in) โ†’ evaluate_results
235
+ 7. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‰๊ฐ€:
236
+ - ๋ถ€์กฑ & refinement_count < 1: refine_search โ†’ classify_intent (๋ฃจํ”„)
237
+ - ์ถฉ๋ถ„ or refinement_count >= 1: search_subgraph
238
+ 8. search_subgraph (filter โ†’ summarize)
239
+ 9. search_subgraph โ†’ generate_answer
240
+ 10. generate_answer ํ›„ ๋ถ„๊ธฐ:
241
+ - is_multi_question: combine_answers โ†’ END
242
+ - ๋‹จ์ผ ์งˆ๋ฌธ: END
243
+
244
+ ํ•ต์‹ฌ ๊ฐœ์„ ์‚ฌํ•ญ (Phase 4):
245
+ - โœ… create_plan์„ START๋กœ ์ด๋™ (์งˆ๋ฌธ ๊ฐœ์ˆ˜ ๋จผ์ € ๊ฐ์ง€)
246
+ - โœ… Send API๋กœ ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ ์žฌ์‚ฌ์šฉ (์ฝ”๋“œ ์ค‘๋ณต ์—†์Œ)
247
+ - โœ… ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ ์‹œ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
248
+ - โœ… Reducer ํŒจํ„ด์œผ๋กœ ์ž๋™ fan-in
249
+
250
+ Returns:
251
+ ๊ตฌ์„ฑ๋œ StateGraph (์ปดํŒŒ์ผ ์ „)
252
+ """
253
+ # ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ
254
+ graph = StateGraph(AgentState)
255
+
256
+ # Phase 4: ๊ณ„ํš ์ˆ˜๋ฆฝ (START ์งํ›„)
257
+ graph.add_node("create_plan", create_plan_node)
258
+ graph.add_node("handle_too_many_questions", handle_too_many_questions_node)
259
+ graph.add_node("initiate_dynamic_search", initiate_dynamic_search_node)
260
+
261
+ # Phase 2: ์งˆ๋ฌธ ๋ถ„์„ & ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ์ฒ˜๋ฆฌ
262
+ graph.add_node("analyze_question", analyze_question_node)
263
+ graph.add_node("generate_with_history", generate_with_history_node)
264
+
265
+ # ์บ์‹œ ๊ด€๋ จ
266
+ graph.add_node("check_cache", check_cache_node)
267
+ graph.add_node("return_cached_answer", return_cached_answer_node)
268
+
269
+ # ์˜๋„ ๋ถ„๋ฅ˜
270
+ graph.add_node("classify_intent", classify_intent_node)
271
+
272
+ # Send API๋ฅผ ์œ„ํ•œ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ ๋…ธ๋“œ
273
+ graph.add_node("search_stackoverflow", search_stackoverflow_node)
274
+ graph.add_node("search_github", search_github_node)
275
+ graph.add_node("search_official_docs", search_official_docs_node)
276
+
277
+ # Phase 3: ๊ฒฐ๊ณผ ์ˆ˜์ง‘ ๋ฐ ํ‰๊ฐ€
278
+ graph.add_node("collect_results", collect_results_node)
279
+ graph.add_node("evaluate_results", evaluate_results_node)
280
+ graph.add_node("refine_search", refine_search_node)
281
+
282
+ # ์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ
283
+ graph.add_node("generate_answer", generate_answer_node)
284
+
285
+ # Phase 4: ๋‹ค์ค‘ ์งˆ๋ฌธ ๋‹ต๋ณ€ ํ†ตํ•ฉ
286
+ graph.add_node("combine_answers", combine_answers_node)
287
+ graph.add_node("run_single_question_worker", run_single_question_worker_node)
288
+
289
+ # ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ (ํ•„ํ„ฐ๋ง & ์š”์•ฝ)
290
+ search_subgraph = build_search_subgraph()
291
+ graph.add_node("search_subgraph", search_subgraph)
292
+
293
+ # ===== ์—ฃ์ง€ ๊ตฌ์„ฑ =====
294
+
295
+ # 1. START โ†’ create_plan (Phase 4: ์ง„์ž…์  ๋ณ€๊ฒฝ)
296
+ graph.add_edge(START, "create_plan")
297
+
298
+ # 2. create_plan โ†’ ๋ถ„๊ธฐ (Phase 4: ์งˆ๋ฌธ ์œ ํ˜•๋ณ„ ๋ถ„๊ธฐ)
299
+ graph.add_conditional_edges(
300
+ "create_plan",
301
+ route_after_plan,
302
+ {
303
+ "analyze_question": "analyze_question",
304
+ "initiate_dynamic_search": "initiate_dynamic_search",
305
+ "handle_too_many_questions": "handle_too_many_questions",
306
+ }
307
+ )
308
+
309
+ # 3. handle_too_many_questions โ†’ END
310
+ graph.add_edge("handle_too_many_questions", END)
311
+
312
+ # 4. initiate_dynamic_search๋Š” Send ๋ฆฌํ„ด (๊ฐ Send๊ฐ€ analyze_question์œผ๋กœ)
313
+ # ์‹ค์ œ fan-out์€ conditional edge ํ•จ์ˆ˜์—์„œ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•จ
314
+ graph.add_conditional_edges(
315
+ "initiate_dynamic_search",
316
+ fanout_multi_questions,
317
+ )
318
+ # multi-question worker๋“ค์ด ๋๋‚˜๋ฉด reducer(multi_answers)์— ๋ชจ์ธ ๊ฒฐ๊ณผ๋ฅผ ํ•ฉ์นฉ๋‹ˆ๋‹ค.
319
+ # Fan-in: ๋‘ worker๊ฐ€ ๋ชจ๋‘ ์ด edge๋กœ ๋“ค์–ด์˜ค๋ฉด combine_answers๋Š” 1ํšŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
320
+ graph.add_edge("run_single_question_worker", "combine_answers")
321
+
322
+ # 5. ์งˆ๋ฌธ ๋ถ„์„ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ
323
+ graph.add_conditional_edges(
324
+ "analyze_question",
325
+ route_after_analysis,
326
+ {
327
+ "generate_with_history": "generate_with_history",
328
+ "check_cache": "check_cache",
329
+ }
330
+ )
331
+
332
+ # 6. ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ โ†’ END
333
+ graph.add_edge("generate_with_history", END)
334
+
335
+ # 7. ์บ์‹œ ํ™•์ธ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ (Phase 4: create_plan ์ œ๊ฑฐ๋จ)
336
+ graph.add_conditional_edges(
337
+ "check_cache",
338
+ route_after_cache,
339
+ {
340
+ "return_cached_answer": "return_cached_answer",
341
+ "classify_intent": "classify_intent",
342
+ }
343
+ )
344
+
345
+ # 8. ์บ์‹œ ํžˆํŠธ ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ
346
+ graph.add_edge("return_cached_answer", END)
347
+
348
+ # 9. Send API๋ฅผ ์‚ฌ์šฉํ•œ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ (fan-out)
349
+ graph.add_conditional_edges(
350
+ "classify_intent",
351
+ initiate_parallel_search,
352
+ )
353
+
354
+ # 10. ๋ชจ๋“  ๊ฒ€์ƒ‰ ๋…ธ๋“œ โ†’ collect_results (fan-in)
355
+ graph.add_edge("search_stackoverflow", "collect_results")
356
+ graph.add_edge("search_github", "collect_results")
357
+ graph.add_edge("search_official_docs", "collect_results")
358
+
359
+ # 11. collect_results โ†’ evaluate_results
360
+ graph.add_edge("collect_results", "evaluate_results")
361
+
362
+ # 12. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‰๊ฐ€์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ (Phase 3: refine_search ์ถ”๊ฐ€)
363
+ graph.add_conditional_edges(
364
+ "evaluate_results",
365
+ route_after_evaluation,
366
+ {
367
+ "refine_search": "refine_search",
368
+ "search_subgraph": "search_subgraph",
369
+ }
370
+ )
371
+
372
+ # 13. ์ฟผ๋ฆฌ ๊ฐœ์„  โ†’ ์˜๋„ ๋ถ„๋ฅ˜ (๋ฃจํ”„)
373
+ graph.add_edge("refine_search", "classify_intent")
374
+
375
+ # 14. ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ โ†’ ์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ
376
+ graph.add_edge("search_subgraph", "generate_answer")
377
+
378
+ # 15. ์ตœ์ข… ๋‹ต๋ณ€ ํ›„ ๋ถ„๊ธฐ (Phase 4: ๋‹ค์ค‘ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ)
379
+ graph.add_conditional_edges(
380
+ "generate_answer",
381
+ route_after_generate,
382
+ {
383
+ "combine_answers": "combine_answers",
384
+ END: END
385
+ }
386
+ )
387
+
388
+ # 16. combine_answers โ†’ ์ข…๋ฃŒ
389
+ graph.add_edge("combine_answers", END)
390
+
391
+ return graph
392
+
393
+
394
+ def create_agent(enable_checkpointing: bool = True):
395
+ """
396
+ CodeWeaver ์—์ด์ „ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ปดํŒŒ์ผํ•ฉ๋‹ˆ๋‹ค.
397
+
398
+ Args:
399
+ enable_checkpointing: ์ฒดํฌํฌ์ธํŠธ ํ™œ์„ฑํ™” ์—ฌ๋ถ€
400
+ - True: MemorySaver ์‚ฌ์šฉ (๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ์šฉ)
401
+ - False: ์ฒดํฌํฌ์ธํŠธ ์—†์ด ์‹คํ–‰ (์ƒํƒœ ์ €์žฅ ๋ถˆ๊ฐ€)
402
+
403
+ Returns:
404
+ ์ปดํŒŒ์ผ๋œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๊ทธ๋ž˜ํ”„
405
+
406
+ Note:
407
+ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” MemorySaver ๋Œ€์‹ 
408
+ PostgresSaver, SqliteSaver ๋“ฑ ์˜๊ตฌ ์ €์žฅ์†Œ ์‚ฌ์šฉ ๊ถŒ์žฅ
409
+ """
410
+ graph = build_agent_graph()
411
+
412
+ if enable_checkpointing:
413
+ # ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฒดํฌํฌ์ธํ„ฐ (ํ”„๋กœ๋•์…˜์—์„œ๋Š” DB ์‚ฌ์šฉ ๊ถŒ์žฅ)
414
+ memory = MemorySaver()
415
+ return graph.compile(checkpointer=memory)
416
+ else:
417
+ return graph.compile()
418
+
419
+
420
+ # ์—์ด์ „ํŠธ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ (๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹œ ์ž๋™ ์ƒ์„ฑ)
421
+ agent = create_agent(enable_checkpointing=True)
422
+
CodeWeaver/src/agent/nodes.py ADDED
@@ -0,0 +1,1387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeWeaver LangGraph ๋…ธ๋“œ ๊ตฌํ˜„.
3
+
4
+ ๊ฐ ๋…ธ๋“œ๋Š” AgentState๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์—…๋ฐ์ดํŠธ๋œ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
5
+ ๋ชจ๋“  ๋…ธ๋“œ๋Š” LangSmith๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ์ถ”์ ๋ฉ๋‹ˆ๋‹ค.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import os
11
+ from typing import List, Literal, Optional
12
+
13
+ from langchain_core.messages import HumanMessage, SystemMessage
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, SearchResult
19
+ from src.agent.state import _MULTI_ANS_RESET_TOKEN # reset token for multi_answers reducer
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
+ @trace_node("analyze_question")
41
+ async def analyze_question_node(state: AgentState) -> dict:
42
+ """
43
+ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ์œ ํ˜•์„ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์บ์‹œ ์ ๊ฒฉ์„ฑ์„ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
44
+
45
+ Phase 2: Question Analysis & Cache Eligibility Decision
46
+
47
+ ๋ถ„๋ฅ˜:
48
+ - followup: ์ด์ „ ๋Œ€ํ™”์— ์˜์กดํ•˜๋Š” ํ›„์† ์งˆ๋ฌธ
49
+ - cache_candidate: ๋…๋ฆฝ์ ์ด๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์งˆ๋ฌธ
50
+ - new_search: ๋…๋ฆฝ์ ์ด์ง€๋งŒ ์บ์‹œํ•˜์ง€ ์•Š์„ ์งˆ๋ฌธ (์‹œ๊ฐ„ ๋ฏผ๊ฐ ๋“ฑ)
51
+ """
52
+ user_question = state.user_question
53
+ messages = state.messages
54
+
55
+
56
+ # ๋Œ€ํ™” ๋งฅ๋ฝ ๊ตฌ์„ฑ
57
+ has_history = messages and len(messages) > 1
58
+ context_info = ""
59
+
60
+ if has_history:
61
+ context_info = "\n์ด์ „ ๋Œ€ํ™” ๋งฅ๋ฝ:\n"
62
+ for msg in messages[-4:-1]: # ํ˜„์žฌ ์งˆ๋ฌธ ์ œ์™ธ ์ตœ๊ทผ 3๊ฐœ
63
+ if hasattr(msg, 'type') and hasattr(msg, 'content'):
64
+ role = "์‚ฌ์šฉ์ž" if msg.type == "human" else "AI"
65
+ context_info += f"{role}: {msg.content[:100]}\n"
66
+
67
+ analysis_prompt = f"""์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ์œ ํ˜•์„ ๋ถ„๋ฅ˜ํ•˜๊ณ , ์บ์‹œ ์ ๊ฒฉ์„ฑ์„ ํŒ๋‹จํ•˜์„ธ์š”.
68
+
69
+ {context_info}
70
+ ํ˜„์žฌ ์งˆ๋ฌธ: {user_question}
71
+
72
+ ๋ถ„๋ฅ˜ ๊ธฐ์ค€:
73
+
74
+ 1. **clarification** (๋ณด์ถฉ/ํ˜•์‹ ๋ณ€๊ฒฝ ์š”์ฒญ)
75
+ - ์ด์ „ ๋‹ต๋ณ€/๋Œ€ํ™” ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ "์„ค๋ช… ๋ฐฉ์‹"์„ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ณด์ถฉ์„ ์š”์ฒญ
76
+ - ์˜ˆ: "์ข€ ๋” ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ด์ค˜", "์˜ˆ์ œ ์ฝ”๋“œ๋กœ ๋ณด์—ฌ์ค˜", "ํ•œ ์ค„๋กœ ์š”์•ฝํ•ด์ค˜", "๋‹ค์‹œ ์„ค๋ช…ํ•ด์ค˜"
77
+ - ์›์น™: ๊ฒ€์ƒ‰/์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€
78
+ - should_cache = false, canonical_question = null
79
+
80
+ 2. **new_topic** (๋Œ€ํ™” ์ค‘ ์ƒˆ ๊ฐœ๋… ์งˆ๋ฌธ)
81
+ - ๋Œ€ํ™”๊ฐ€ ์ด์–ด์ง€๋Š” ์ค‘์ด์ง€๋งŒ, ์งˆ๋ฌธ ์ž์ฒด๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์„ฑ๋ฆฝํ•˜๋Š” '์ƒˆ ๊ฐœ๋…/์ •์˜/๋น„๊ต/์‚ฌ์šฉ๋ฒ•' ์งˆ๋ฌธ
82
+ - ์˜ˆ: (React ์ด์•ผ๊ธฐ ์ค‘) "Event Listener๋Š” ๋ญ์•ผ?", "CORS๊ฐ€ ๋ญ์•ผ?"
83
+ - ์›์น™: ๊ฒ€์ƒ‰ + ์บ์‹œ ์ €์žฅ ๊ฐ€์น˜๊ฐ€ ํผ
84
+ - should_cache = true (๊ธฐ๋ณธ), canonical_question ์ƒ์„ฑ
85
+
86
+ 3. **independent** (์™„์ „ ๋…๋ฆฝ ์งˆ๋ฌธ)
87
+ - ์ด์ „ ๋Œ€ํ™” ์—†์ด๋„ ์ดํ•ด ๊ฐ€๋Šฅํ•œ ์ผ๋ฐ˜ ์งˆ๋ฌธ
88
+ - ์˜ˆ: "Spring Security๊ฐ€ ๋ญ์•ผ?", "Docker Compose ์‚ฌ์šฉ๋ฒ•์€?"
89
+ - ์›์น™: ๊ฒ€์ƒ‰ + ์บ์‹œ ์ €์žฅ ๊ฐ€์น˜๊ฐ€ ํผ
90
+ - should_cache = true (๊ธฐ๋ณธ), canonical_question ์ƒ์„ฑ
91
+
92
+ ๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
93
+ {{
94
+ "question_type": "clarification|new_topic|independent",
95
+ "should_cache": true|false,
96
+ "reasoning": "๋ถ„๋ฅ˜ ์ด์œ  1-2๋ฌธ์žฅ",
97
+ "canonical_question": "์บ์‹œํ•  ์ •๊ทœํ™”๋œ ์งˆ๋ฌธ (should_cache๊ฐ€ true์ธ ๊ฒฝ์šฐ์—๋งŒ, ์•„๋‹ˆ๋ฉด null)"
98
+ }}
99
+
100
+ JSON ์™ธ์— ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋Š” ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”."""
101
+
102
+ try:
103
+ messages_to_llm = [HumanMessage(content=analysis_prompt)]
104
+ response = llm.invoke(messages_to_llm)
105
+
106
+ # JSON ํŒŒ์‹ฑ
107
+ import json
108
+ response_text = response.content.strip()
109
+
110
+ # JSON ๋ธ”๋ก ์ถ”์ถœ (๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก ์ œ๊ฑฐ)
111
+ if "```json" in response_text:
112
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
113
+ elif "```" in response_text:
114
+ response_text = response_text.split("```")[1].split("```")[0].strip()
115
+
116
+ analysis = json.loads(response_text)
117
+
118
+ question_type = analysis.get("question_type", "independent")
119
+ should_cache = analysis.get("should_cache", False)
120
+ reasoning = analysis.get("reasoning", "")
121
+ canonical_question = analysis.get("canonical_question", user_question)
122
+
123
+ # ์œ ํšจ์„ฑ ๊ฒ€์ฆ
124
+ if question_type not in ["clarification", "new_topic", "independent"]:
125
+ question_type = "independent"
126
+
127
+ # 1์ฐจ ์ •์ฑ… ๋ณด์ •: clarification์€ ์บ์‹œ ๊ธˆ์ง€
128
+ if question_type == "clarification":
129
+ should_cache = False
130
+ canonical_question = None
131
+ else:
132
+ # new_topic/independent๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์บ์‹œ ๊ฐ€๋Šฅ
133
+ if canonical_question is None or (isinstance(canonical_question, str) and not canonical_question.strip()):
134
+ canonical_question = user_question
135
+
136
+ # ์‹คํ–‰(run) ์‹œ์ž‘๋งˆ๋‹ค step ๋กœ๊ทธ๋ฅผ ๋ฆฌ์…‹ํ•˜๊ณ , ์ด๋ฒˆ ์‹คํ–‰์˜ step๋งŒ ๋ˆ„์ ๋˜๊ฒŒ ํ•จ
137
+ steps_delta = [
138
+ "__RESET_STEPS__",
139
+ f"๐Ÿ” ์งˆ๋ฌธ ๋ถ„์„: {question_type} (์บ์‹œ ์—ฌ๋ถ€: {should_cache})",
140
+ ]
141
+
142
+
143
+ return {
144
+ "question_type": question_type,
145
+ "should_cache": should_cache,
146
+ "analysis_reasoning": reasoning,
147
+ "canonical_question": canonical_question if should_cache else None,
148
+ "intermediate_steps": steps_delta
149
+ }
150
+
151
+ except Exception as e:
152
+ logger.error("์งˆ๋ฌธ ๋ถ„์„ ์‹คํŒจ: %s", e, exc_info=True)
153
+
154
+ # ๊ธฐ๋ณธ๊ฐ’: ๋…๋ฆฝ ์งˆ๋ฌธ์œผ๋กœ ๊ฐ„์ฃผ
155
+ steps_delta = [
156
+ "__RESET_STEPS__",
157
+ "โš ๏ธ ์งˆ๋ฌธ ๋ถ„์„ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: independent",
158
+ ]
159
+
160
+ return {
161
+ "question_type": "independent",
162
+ "should_cache": True,
163
+ "analysis_reasoning": "๋ถ„์„ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ",
164
+ "canonical_question": user_question,
165
+ "intermediate_steps": steps_delta
166
+ }
167
+
168
+
169
+ @trace_node("check_cache")
170
+ async def check_cache_node(state: AgentState) -> dict:
171
+ """
172
+ ๋ฒกํ„ฐ DB ์บ์‹œ์—์„œ ์œ ์‚ฌํ•œ ์งˆ๋ฌธ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
173
+
174
+ threshold 0.85 ์ด์ƒ์ธ ๊ฒฝ์šฐ ์บ์‹œ ํžˆํŠธ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
175
+ """
176
+ question_for_lookup = state.canonical_question or state.user_question
177
+ logger.info("์บ์‹œ ํ™•์ธ ์ค‘: %s", question_for_lookup[:50])
178
+
179
+ try:
180
+ cached_result = await qdrant_manager.search_cache(
181
+ question=question_for_lookup,
182
+ threshold=0.85
183
+ )
184
+
185
+ updates = {}
186
+ steps_delta: List[str] = []
187
+
188
+ if cached_result:
189
+ updates["cached_result"] = cached_result
190
+ steps_delta.append(f"โœ… ์บ์‹œ ํžˆํŠธ (๋‹ต๋ณ€ ๊ธธ์ด: {len(cached_result)}์ž)")
191
+ logger.info("์บ์‹œ ํžˆํŠธ")
192
+ else:
193
+ updates["cached_result"] = None
194
+ steps_delta.append("โŒ ์บ์‹œ ๋ฏธ์Šค: ์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰ ํ•„์š”")
195
+ logger.info("์บ์‹œ ๋ฏธ์Šค")
196
+
197
+ except Exception as e:
198
+ logger.error("์บ์‹œ ํ™•์ธ ์‹คํŒจ: %s", e, exc_info=True)
199
+ updates["cached_result"] = None
200
+ steps_delta.append(f"โš ๏ธ ์บ์‹œ ํ™•์ธ ์˜ค๋ฅ˜: {str(e)}")
201
+
202
+ updates["intermediate_steps"] = steps_delta
203
+ return updates
204
+
205
+
206
+ @trace_node("create_plan")
207
+ def create_plan_node(state: AgentState) -> dict:
208
+ """
209
+ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ์œ ํ˜•๊ณผ ๊ฐœ์ˆ˜๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
210
+
211
+ Phase 4: Dynamic Parallel Search
212
+ - single_topic: ํ•˜๋‚˜์˜ ์ฃผ์ œ (๊ธฐ์กด ๊ทธ๋ž˜ํ”„ ์‹คํ–‰)
213
+ - multiple_questions: ๋…๋ฆฝ ์งˆ๋ฌธ 2๊ฐœ (Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ์‹คํ–‰)
214
+ - too_many: ๋…๋ฆฝ ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ (์—๋Ÿฌ ๋ฉ”์‹œ์ง€)
215
+
216
+ LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ: ๋…ธ๋“œ๋Š” ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ ์ˆ˜ํ–‰ (๊ณ„ํš ์ˆ˜๋ฆฝ)
217
+ """
218
+ user_question = state.user_question
219
+ logger.info("์งˆ๋ฌธ ๋ถ„์„ ๋ฐ ๊ณ„ํš ์ˆ˜๋ฆฝ ์ค‘: %s", user_question[:50])
220
+
221
+ def _extract_question_candidates(text: str) -> List[str]:
222
+ """์ž…๋ ฅ ๋ฌธ์ž์—ด์—์„œ '์งˆ๋ฌธ ํ›„๋ณด'๋ฅผ ์ตœ๋Œ€ํ•œ ๋ณด์ˆ˜์ ์œผ๋กœ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค(3๊ฐœ ์ด์ƒ ๊ฐ์ง€์šฉ)."""
223
+ import re
224
+
225
+ if not text:
226
+ return []
227
+
228
+ t = text.strip()
229
+ # 1) ๋ฌผ์Œํ‘œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฆฌ (๊ฐ€์žฅ ์‹ ๋ขฐ๋„ ๋†’์Œ)
230
+ parts = re.split(r"[?๏ผŸ]+", t)
231
+ candidates = [p.strip() for p in parts if p.strip()]
232
+ if len(candidates) >= 2 and re.search(r"[?๏ผŸ]", t):
233
+ # ๋ฌผ์Œํ‘œ๊ฐ€ ์กด์žฌํ•  ๋•Œ๋งŒ ์ด ๊ทœ์น™์„ ์‹ ๋ขฐ
234
+ return candidates
235
+
236
+ # 2) ์ค„๋ฐ”๊ฟˆ/๋ฒˆํ˜ธ ๋งค๊ธฐ๊ธฐ ๊ธฐ๋ฐ˜ (๋‹ค์ค‘ ์งˆ๋ฌธ ์ž…๋ ฅ ํŒจํ„ด)
237
+ lines = [ln.strip() for ln in re.split(r"[\r\n]+", t) if ln.strip()]
238
+ numbered = []
239
+ for ln in lines:
240
+ if re.match(r"^\s*(\d+[\.\)]|[-*])\s+", ln):
241
+ numbered.append(re.sub(r"^\s*(\d+[\.\)]|[-*])\s+", "", ln).strip())
242
+ if len(numbered) >= 2:
243
+ return numbered
244
+
245
+ # 3) ๊ตฌ๋ถ„์ž ๊ธฐ๋ฐ˜(์„ธ๋ฏธ์ฝœ๋ก ) โ€” ๋ณด์กฐ
246
+ semi = [p.strip() for p in t.split(";") if p.strip()]
247
+ if len(semi) >= 2:
248
+ return semi
249
+
250
+ return [t]
251
+
252
+ def _hard_guard_too_many(text: str) -> Optional[dict]:
253
+ """
254
+ ํ•˜๋“œ ๊ฐ€๋“œ: ์‚ฌ์šฉ์ž๊ฐ€ '์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ'์„ ํ•œ ๋ฒˆ์— ๋˜์ง„ ๊ฒƒ์œผ๋กœ ํ™•์‹คํ•œ ๊ฒฝ์šฐ,
255
+ LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ too_many๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
256
+ """
257
+ import re
258
+
259
+ if not text:
260
+ return None
261
+
262
+ # ๊ฐ€์žฅ ํ™•์‹คํ•œ ๊ธฐ์ค€: ๋ฌผ์Œํ‘œ๊ฐ€ 3๊ฐœ ์ด์ƒ
263
+ qmarks = len(re.findall(r"[?๏ผŸ]", text))
264
+ if qmarks >= 3:
265
+ candidates = _extract_question_candidates(text)
266
+ msg = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์€ ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 2๊ฐœ๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ค‘์š”ํ•œ 2๊ฐœ๋งŒ ๊ณจ๋ผ์„œ ๋‹ค์‹œ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”."
267
+ return {
268
+ "case": "too_many",
269
+ "sub_questions": candidates,
270
+ "reasoning": f"๋ฌผ์Œํ‘œ๊ฐ€ {qmarks}๊ฐœ๋กœ, 3๊ฐœ ์ด์ƒ์˜ ๋…๋ฆฝ ์งˆ๋ฌธ์œผ๋กœ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.",
271
+ "error_message": msg,
272
+ "steps_note": f"โš ๏ธ ์งˆ๋ฌธ ์ˆ˜ ์ดˆ๊ณผ ๊ฐ์ง€(๋ฌผ์Œํ‘œ {qmarks}๊ฐœ) โ†’ too_many๋กœ ๊ฐ•์ œ",
273
+ }
274
+
275
+ # ๋ฒˆํ˜ธ ๋งค๊ธฐ๊ธฐ/๋ฆฌ์ŠคํŠธ๋กœ 3๊ฐœ ์ด์ƒ
276
+ candidates = _extract_question_candidates(text)
277
+ if len(candidates) >= 3:
278
+ msg = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์€ ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 2๊ฐœ๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ค‘์š”ํ•œ 2๊ฐœ๋งŒ ๊ณจ๋ผ์„œ ๋‹ค์‹œ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”."
279
+ return {
280
+ "case": "too_many",
281
+ "sub_questions": candidates,
282
+ "reasoning": f"์งˆ๋ฌธ ํ›„๋ณด๊ฐ€ {len(candidates)}๊ฐœ๋กœ ๊ฐ์ง€๋˜์–ด 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ์œผ๋กœ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.",
283
+ "error_message": msg,
284
+ "steps_note": f"โš ๏ธ ์งˆ๋ฌธ ์ˆ˜ ์ดˆ๊ณผ ๊ฐ์ง€(ํ›„๋ณด {len(candidates)}๊ฐœ) โ†’ too_many๋กœ ๊ฐ•์ œ",
285
+ }
286
+
287
+ return None
288
+
289
+ # ํ•˜๋“œ ๊ฐ€๋“œ(๊ฒฐ์ •๋ก ์ ) โ€” LLM์ด ์ž˜๋ชป ๋ถ„๋ฅ˜ํ•˜๋”๋ผ๋„ 3๊ฐœ ์ด์ƒ์ด๋ฉด ๋ฌด์กฐ๊ฑด ์ฐจ๋‹จ
290
+ hard = _hard_guard_too_many(user_question)
291
+ if hard:
292
+ steps_delta = [
293
+ f"๐Ÿ“‹ ๊ณ„ํš ํƒ€์ž…: {hard['case']}",
294
+ f" ์„œ๋ธŒ์งˆ๋ฌธ: {len(hard['sub_questions'])}๊ฐœ",
295
+ f" ์ด์œ : {hard['reasoning']}",
296
+ hard["steps_note"],
297
+ ]
298
+ logger.info("๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ(ํ•˜๋“œ ๊ฐ€๋“œ): too_many, %d๊ฐœ ์„œ๋ธŒ์งˆ๋ฌธ", len(hard["sub_questions"]))
299
+ return {
300
+ "plan": {
301
+ "case": hard["case"],
302
+ "sub_questions": hard["sub_questions"],
303
+ "reasoning": hard["reasoning"],
304
+ "error_message": hard["error_message"],
305
+ },
306
+ "is_multi_question": False,
307
+ "sub_question_index": 0,
308
+ "sub_question_text": None,
309
+ "original_multi_question": None,
310
+ "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
311
+ "intermediate_steps": steps_delta,
312
+ }
313
+
314
+ plan_prompt = f"""์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ์œ ํ˜•๊ณผ ๊ฐœ์ˆ˜๋ฅผ ํŒ๋‹จํ•˜์„ธ์š”.
315
+
316
+ ์งˆ๋ฌธ: {user_question}
317
+
318
+ **์ค‘์š”**: sub_questions์˜ ์šฉ๋„๋Š” case์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!
319
+
320
+ **Case 1: single_topic** (ํ•˜๋‚˜์˜ ์ฃผ์ œ)
321
+ - ์˜ˆ: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„"
322
+ โ†’ sub_questions: ["๊ฐœ๋…", "๊ตฌํ˜„", "์˜ˆ์ œ"]
323
+ โ†’ ์šฉ๋„: ๋‹ต๋ณ€ ์„น์…˜ ๊ตฌ์กฐ (๊ฒ€์ƒ‰์€ ์›๋ณธ ์งˆ๋ฌธ์œผ๋กœ 1ํšŒ๋งŒ)
324
+ โ†’ ๊ฒ€์ƒ‰: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„"
325
+
326
+ - ์˜ˆ: "React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ"
327
+ โ†’ sub_questions: ["hooks๋ž€", "์ฃผ์š” hooks", "์‹ค๋ฌด ํŒจํ„ด"]
328
+ โ†’ ์šฉ๋„: ๋‹ต๋ณ€ ์„น์…˜ ๊ตฌ์กฐ
329
+ โ†’ ๊ฒ€์ƒ‰: "React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ"
330
+
331
+ **Case 2: multiple_questions** (์—ฌ๋Ÿฌ ๋…๋ฆฝ ์งˆ๋ฌธ, ์ตœ๋Œ€ 2๊ฐœ)
332
+ - ์˜ˆ: "JWT๊ฐ€ ๋ญ์•ผ? CORS๋Š”?"
333
+ โ†’ sub_questions: ["JWT๊ฐ€ ๋ญ์•ผ?", "CORS๋Š”?"]
334
+ โ†’ ์šฉ๋„: ๊ฐ ์งˆ๋ฌธ๋งˆ๋‹ค ๋ณ„๋„ ๊ฒ€์ƒ‰
335
+ โ†’ ๊ฒ€์ƒ‰: "JWT๊ฐ€ ๋ญ์•ผ?" (1ํšŒ), "CORS๋Š”?" (1ํšŒ)
336
+
337
+ - ์˜ˆ: "Docker ์‚ฌ์šฉ๋ฒ•์€? Redis ์„ค์น˜๋Š”?"
338
+ โ†’ sub_questions: ["Docker ์‚ฌ์šฉ๋ฒ•์€?", "Redis ์„ค์น˜๋Š”?"]
339
+ โ†’ ์šฉ๋„: ๊ฐ ์งˆ๋ฌธ๋งˆ๋‹ค ๋ณ„๋„ ๊ฒ€์ƒ‰
340
+
341
+ **Case 3: too_many** (3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ)
342
+ - ์˜ˆ: "JWT? CORS? Docker?"
343
+ โ†’ ๋„ˆ๋ฌด ๋งŽ์•„์„œ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€
344
+ โ†’ error_message ์ œ๊ณต
345
+
346
+ ๊ทœ์น™:
347
+ - single_topic: sub_questions๋Š” ์งง์€ ํ‚ค์›Œ๋“œ/๊ตฌ์ ˆ (1-5๊ฐœ)
348
+ - multiple_questions: sub_questions๋Š” ์™„์ „ํ•œ ๋ฌธ์žฅ (์ •ํ™•ํžˆ 2๊ฐœ๋งŒ)
349
+ - too_many: 3๊ฐœ ์ด์ƒ์ด๋ฉด ์ด ์ผ€์ด์Šค๋กœ ๋ถ„๋ฅ˜
350
+
351
+ ๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
352
+ {{
353
+ "case": "single_topic|multiple_questions|too_many",
354
+ "sub_questions": [...],
355
+ "reasoning": "์ด ์ผ€์ด์Šค๋กœ ํŒ๋‹จํ•œ ์ด์œ ",
356
+ "error_message": "..." (too_many์ธ ๊ฒฝ์šฐ๋งŒ, ๊ทธ ์™ธ๋Š” ๋นˆ ๋ฌธ์ž์—ด)
357
+ }}
358
+
359
+ JSON ์™ธ์— ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋Š” ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”."""
360
+
361
+ try:
362
+ import json
363
+
364
+ messages_to_llm = [HumanMessage(content=plan_prompt)]
365
+ response = llm.invoke(messages_to_llm)
366
+
367
+ # JSON ํŒŒ์‹ฑ
368
+ response_text = response.content.strip()
369
+
370
+ # JSON ๋ธ”๋ก ์ถ”์ถœ (๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก ์ œ๊ฑฐ)
371
+ if "```json" in response_text:
372
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
373
+ elif "```" in response_text:
374
+ response_text = response_text.split("```")[1].split("```")[0].strip()
375
+
376
+ plan_data = json.loads(response_text)
377
+
378
+ case = plan_data.get("case", "single_topic")
379
+ sub_questions = plan_data.get("sub_questions", [user_question])
380
+ reasoning = plan_data.get("reasoning", "")
381
+ error_message = plan_data.get("error_message", "")
382
+
383
+ # LLM ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์€ ๋’ค์—๋„ ํ•œ ๋ฒˆ ๋” ํ•˜๋“œ ๊ฐ€๋“œ ์ ์šฉ (์•ˆ์ „์žฅ์น˜)
384
+ hard2 = _hard_guard_too_many(user_question)
385
+ if hard2:
386
+ case = hard2["case"]
387
+ sub_questions = hard2["sub_questions"]
388
+ reasoning = hard2["reasoning"]
389
+ error_message = hard2["error_message"]
390
+
391
+ # ์œ ํšจ์„ฑ ๊ฒ€์ฆ
392
+ if not sub_questions or len(sub_questions) == 0:
393
+ sub_questions = [user_question]
394
+ case = "single_topic"
395
+
396
+ # multiple_questions์ผ ๋•Œ 2๊ฐœ ์ œํ•œ ๊ฐ•์ œ (๋‹จ, 3๊ฐœ ์ด์ƒ์€ ์œ„ ํ•˜๋“œ ๊ฐ€๋“œ์—์„œ too_many๋กœ ์ฒ˜๋ฆฌ๋จ)
397
+ if case == "multiple_questions" and len(sub_questions) > 2:
398
+ sub_questions = sub_questions[:2]
399
+ reasoning += " (์งˆ๋ฌธ ์ˆ˜ ์ œํ•œ: ์ตœ๋Œ€ 2๊ฐœ)"
400
+
401
+ steps_delta = [
402
+ f"๐Ÿ“‹ ๊ณ„ํš ํƒ€์ž…: {case}",
403
+ f" ์„œ๋ธŒ์งˆ๋ฌธ: {len(sub_questions)}๊ฐœ",
404
+ f" ์ด์œ : {reasoning}"
405
+ ]
406
+
407
+ logger.info("๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ: %s, %d๊ฐœ ์„œ๋ธŒ์งˆ๋ฌธ", case, len(sub_questions))
408
+
409
+ # NOTE: ์ด ๊ทธ๋ž˜ํ”„๋Š” ์ฒดํฌํฌ์ธํŒ…/์Šค๋ ˆ๋“œ ์œ ์ง€๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ,
410
+ # multi_answers๋Š” ๋งค ์‹คํ–‰(run) ์‹œ์ž‘ ์‹œ ๋ฆฌ์…‹ํ•ด์•ผ ์ด์ „ ํ„ด ๋ˆ„์ ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
411
+ return {
412
+ "plan": {
413
+ "case": case,
414
+ "sub_questions": sub_questions,
415
+ "reasoning": reasoning,
416
+ "error_message": error_message
417
+ },
418
+ "is_multi_question": False,
419
+ "sub_question_index": 0,
420
+ "sub_question_text": None,
421
+ "original_multi_question": None,
422
+ "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
423
+ "intermediate_steps": steps_delta
424
+ }
425
+
426
+ except Exception as e:
427
+ logger.error("๊ณ„ํš ์ˆ˜๋ฆฝ ์‹คํŒจ: %s", e, exc_info=True)
428
+
429
+ # ๊ธฐ๋ณธ๊ฐ’: ์›๋ณธ ์งˆ๋ฌธ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
430
+ steps_delta = [
431
+ "โš ๏ธ ๊ณ„ํš ์ˆ˜๋ฆฝ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: single_topic"
432
+ ]
433
+
434
+ return {
435
+ "plan": {
436
+ "case": "single_topic",
437
+ "sub_questions": [user_question],
438
+ "reasoning": "๊ณ„ํš ์ˆ˜๋ฆฝ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ",
439
+ "error_message": ""
440
+ },
441
+ "is_multi_question": False,
442
+ "sub_question_index": 0,
443
+ "sub_question_text": None,
444
+ "original_multi_question": None,
445
+ "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}],
446
+ "intermediate_steps": steps_delta
447
+ }
448
+
449
+
450
+ @trace_node("classify_intent")
451
+ def classify_intent_node(state: AgentState) -> dict:
452
+ """
453
+ LLM์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์˜ ์˜๋„๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.
454
+
455
+ ๋ถ„๋ฅ˜ ์นดํ…Œ๊ณ ๋ฆฌ:
456
+ - debugging: ์—๋Ÿฌ ํ•ด๊ฒฐ, ๋ฒ„๊ทธ ์ˆ˜์ •
457
+ - learning: ๊ฐœ๋… ํ•™์Šต, ์›๋ฆฌ ์ดํ•ด
458
+ - code_review: ์ฝ”๋“œ ๊ฐœ์„ , ๋ฆฌํŒฉํ† ๋ง
459
+ """
460
+ logger.info("์˜๋„ ๋ถ„๋ฅ˜ ์ค‘: %s", state.user_question[:50])
461
+
462
+ classification_prompt = f"""์งˆ๋ฌธ์„ ๋‹ค์Œ ์„ธ ๊ฐ€์ง€ ์˜๋„ ์ค‘ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜ํ•˜์„ธ์š”:
463
+
464
+ 1. debugging: ์—๋Ÿฌ ํ•ด๊ฒฐ, ๋ฒ„๊ทธ ์ˆ˜์ •, ๋ฌธ์ œ ํ•ด๊ฒฐ
465
+ ์˜ˆ: "ImportError๊ฐ€ ๋ฐœ์ƒํ•ด์š”", "์ด ์ฝ”๋“œ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์•„์š”"
466
+
467
+ 2. learning: ๊ฐœ๋… ํ•™์Šต, ์›๋ฆฌ ์ดํ•ด, ํŠœํ† ๋ฆฌ์–ผ
468
+ ์˜ˆ: "async/await๊ฐ€ ๋ญ”๊ฐ€์š”?", "JPA ๋™์ž‘ ์›๋ฆฌ๋Š”?"
469
+
470
+ 3. code_review: ์ฝ”๋“œ ๊ฐœ์„ , ๋ฆฌํŒฉํ† ๋ง, ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
471
+ ์˜ˆ: "์ด ์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•  ๋ฐฉ๋ฒ•์€?", "๋” ๋‚˜์€ ์„ค๊ณ„๋Š”?"
472
+
473
+ ์งˆ๋ฌธ: {state.user_question}
474
+
475
+ ๋ฐ˜๋“œ์‹œ debugging, learning, code_review ์ค‘ ํ•˜๋‚˜๋งŒ ๋‹ตํ•˜์„ธ์š”."""
476
+
477
+ updates = {}
478
+ steps_delta: List[str] = []
479
+
480
+ try:
481
+ messages = [
482
+ SystemMessage(content="๋‹น์‹ ์€ ๊ฐœ๋ฐœ์ž ์งˆ๋ฌธ์„ ๋ถ„๋ฅ˜ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
483
+ HumanMessage(content=classification_prompt)
484
+ ]
485
+
486
+ response = llm.invoke(messages)
487
+ intent_raw = response.content.strip().lower()
488
+
489
+ # ์œ ํšจํ•œ ์˜๋„๋กœ ์ •๊ทœํ™”
490
+ valid_intents = ["debugging", "learning", "code_review"]
491
+ intent = next((i for i in valid_intents if i in intent_raw), "learning")
492
+
493
+ updates["detected_intent"] = intent
494
+ steps_delta.append(f"๐ŸŽฏ ์˜๋„ ๋ถ„๋ฅ˜: {intent}")
495
+ logger.info("์˜๋„ ๋ถ„๋ฅ˜ ์™„๋ฃŒ: %s", intent)
496
+
497
+ except Exception as e:
498
+ logger.error("์˜๋„ ๋ถ„๋ฅ˜ ์‹คํŒจ: %s", e, exc_info=True)
499
+ updates["detected_intent"] = "learning"
500
+ steps_delta.append("โš ๏ธ ์˜๋„ ๋ถ„๋ฅ˜ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: learning")
501
+
502
+ updates["intermediate_steps"] = steps_delta
503
+ return updates
504
+
505
+
506
+ @trace_node("search_stackoverflow")
507
+ def search_stackoverflow_node(state: AgentState) -> dict:
508
+ """
509
+ Stack Overflow์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
510
+
511
+ Send API๋ฅผ ํ†ตํ•œ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰์˜ ์ผ๋ถ€๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
512
+ search_results์™€ intermediate_steps๋Š” Annotated[List, add]๋กœ
513
+ ์ •์˜๋˜์–ด ์žˆ์–ด ์ž๋™์œผ๋กœ ๋จธ์ง€๋ฉ๋‹ˆ๋‹ค.
514
+ """
515
+ intent = state.detected_intent or "learning"
516
+ count = 5 if intent == "debugging" else 3
517
+
518
+ logger.info("Stack Overflow ๊ฒ€์ƒ‰ ์‹œ์ž‘: %d๊ฐœ", count)
519
+
520
+ try:
521
+ results = search_stackoverflow(state.user_question, count)
522
+ logger.info("Stack Overflow์—์„œ %d๊ฐœ ๊ฒฐ๊ณผ ์ˆ˜์ง‘", len(results))
523
+
524
+ # reducer๊ฐ€ ์ž๋™์œผ๋กœ ๋จธ์ง€ํ•˜๋ฏ€๋กœ ์ƒˆ ๊ฒฐ๊ณผ๋งŒ ๋ฐ˜ํ™˜
525
+ return {
526
+ "search_results": results,
527
+ "intermediate_steps": [f"๐Ÿ” Stack Overflow: {len(results)}๊ฐœ ๊ฒฐ๊ณผ"]
528
+ }
529
+ except Exception as e:
530
+ logger.error("Stack Overflow ๊ฒ€์ƒ‰ ์‹คํŒจ: %s", e)
531
+ return {
532
+ "intermediate_steps": [f"โš ๏ธ Stack Overflow ๊ฒ€์ƒ‰ ์‹คํŒจ: {str(e)}"]
533
+ }
534
+
535
+
536
+ @trace_node("search_github")
537
+ def search_github_node(state: AgentState) -> dict:
538
+ """
539
+ GitHub Issues/Discussions์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
540
+
541
+ Send API๋ฅผ ํ†ตํ•œ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰์˜ ์ผ๋ถ€๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
542
+ """
543
+ intent = state.detected_intent or "learning"
544
+ count = 5 if intent == "code_review" else 3 if intent == "learning" else 2
545
+
546
+ logger.info("GitHub ๊ฒ€์ƒ‰ ์‹œ์ž‘: %d๊ฐœ", count)
547
+
548
+ try:
549
+ results = search_github(state.user_question, count)
550
+ logger.info("GitHub์—์„œ %d๊ฐœ ๊ฒฐ๊ณผ ์ˆ˜์ง‘", len(results))
551
+
552
+ # reducer๊ฐ€ ์ž๋™์œผ๋กœ ๋จธ์ง€
553
+ return {
554
+ "search_results": results,
555
+ "intermediate_steps": [f"๐Ÿ” GitHub: {len(results)}๊ฐœ ๊ฒฐ๊ณผ"]
556
+ }
557
+ except Exception as e:
558
+ logger.error("GitHub ๊ฒ€์ƒ‰ ์‹คํŒจ: %s", e)
559
+ return {
560
+ "intermediate_steps": [f"โš ๏ธ GitHub ๊ฒ€์ƒ‰ ์‹คํŒจ: {str(e)}"]
561
+ }
562
+
563
+
564
+ @trace_node("search_official_docs")
565
+ def search_official_docs_node(state: AgentState) -> dict:
566
+ """
567
+ ๊ณต์‹ ๋ฌธ์„œ/Tavily์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
568
+
569
+ Send API๋ฅผ ํ†ตํ•œ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰์˜ ์ผ๋ถ€๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
570
+ """
571
+ intent = state.detected_intent or "learning"
572
+ count = 5 if intent == "learning" else 2
573
+
574
+ logger.info("๊ณต์‹ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์‹œ์ž‘: %d๊ฐœ", count)
575
+
576
+ try:
577
+ results = search_official_docs(state.user_question, count)
578
+ logger.info("๊ณต์‹ ๋ฌธ์„œ์—์„œ %d๊ฐœ ๊ฒฐ๊ณผ ์ˆ˜์ง‘", len(results))
579
+
580
+ # reducer๊ฐ€ ์ž๋™์œผ๋กœ ๋จธ์ง€
581
+ return {
582
+ "search_results": results,
583
+ "intermediate_steps": [f"๐Ÿ” ๊ณต์‹ ๋ฌธ์„œ: {len(results)}๊ฐœ ๊ฒฐ๊ณผ"]
584
+ }
585
+ except Exception as e:
586
+ logger.error("๊ณต์‹ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์‹คํŒจ: %s", e)
587
+ return {
588
+ "intermediate_steps": [f"โš ๏ธ ๊ณต์‹ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์‹คํŒจ: {str(e)}"]
589
+ }
590
+
591
+
592
+ @trace_node("collect_results")
593
+ def collect_results_node(state: AgentState) -> dict:
594
+ """
595
+ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์นด์šดํŠธํ•ฉ๋‹ˆ๋‹ค.
596
+
597
+ Fan-in ํฌ์ธํŠธ: 3๊ฐœ์˜ ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ ๋…ธ๋“œ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋œ ํ›„ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
598
+ LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ: Send API์˜ fan-in ์ง€์ ์—์„œ ๊ฒฐ๊ณผ ์ง‘๊ณ„
599
+ """
600
+ total_results = len(state.search_results)
601
+
602
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜์ง‘ ์™„๋ฃŒ: %d๊ฐœ", total_results)
603
+
604
+ steps_delta = [
605
+ f"๐Ÿ“Š ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜์ง‘: ์ด {total_results}๊ฐœ"
606
+ ]
607
+
608
+ return {
609
+ "intermediate_steps": steps_delta
610
+ }
611
+
612
+
613
+ @trace_node("evaluate_results")
614
+ def evaluate_results_node(state: AgentState) -> dict:
615
+ """
616
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๊ฐœ์ˆ˜์™€ ํ’ˆ์งˆ์„ ๋ชจ๋‘ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
617
+
618
+ ํ‰๊ฐ€ ๊ธฐ์ค€:
619
+ 1. ๊ฐœ์ˆ˜: ์ตœ์†Œ 2๊ฐœ ์ด์ƒ
620
+ 2. ํ’ˆ์งˆ: ํ‰๊ท  relevance_score >= 0.6
621
+ """
622
+ search_results = state.search_results # ์ง์ ‘ ์‚ฌ์šฉ (๋” ์•ˆ์ „)
623
+ refinement_count = state.refinement_count
624
+
625
+ result_count = len(search_results)
626
+
627
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‰๊ฐ€: %d๊ฐœ (๊ฐœ์„  ํšŸ์ˆ˜: %d)", result_count, refinement_count)
628
+
629
+ # ์•ˆ์ „์žฅ์น˜: ์ด๋ฏธ 1ํšŒ ๊ฐœ์„ ํ–ˆ์œผ๋ฉด ๋” ์ด์ƒ ๊ฐœ์„ ํ•˜์ง€ ์•Š์Œ
630
+ if refinement_count >= 1:
631
+ steps_delta = [
632
+ f"โš ๏ธ ์ตœ๋Œ€ ๊ฐœ์„  ํšŸ์ˆ˜ ๋„๋‹ฌ ({refinement_count}ํšŒ), ํ˜„์žฌ ๊ฒฐ๊ณผ๋กœ ์ง„ํ–‰"
633
+ ]
634
+ return {
635
+ "needs_refinement": False,
636
+ "intermediate_steps": steps_delta
637
+ }
638
+
639
+ # 1์ฐจ ํ‰๊ฐ€: ๊ฐœ์ˆ˜
640
+ if result_count < 2:
641
+ steps_delta = [
642
+ f"โš ๏ธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ถ€์กฑ ({result_count}๊ฐœ < 2๊ฐœ), ์ฟผ๋ฆฌ ๊ฐœ์„  ํ•„์š”"
643
+ ]
644
+ return {
645
+ "needs_refinement": True,
646
+ "intermediate_steps": steps_delta
647
+ }
648
+
649
+ # 2์ฐจ ํ‰๊ฐ€: ํ’ˆ์งˆ (relevance_score๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ)
650
+ scored_results = [r for r in search_results if r.relevance_score is not None]
651
+
652
+ if scored_results:
653
+ avg_score = sum(r.relevance_score for r in scored_results) / len(scored_results)
654
+
655
+ # ํ‰๊ท  ์ ์ˆ˜๊ฐ€ 0.5 ๋ฏธ๋งŒ์ด๋ฉด ํ’ˆ์งˆ ๋ถ€์กฑ
656
+ if avg_score < 0.5:
657
+ steps_delta = [
658
+ f"โš ๏ธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๋ถ€์กฑ (ํ‰๊ท  ์ ์ˆ˜: {avg_score:.2f} < 0.5), ์ฟผ๋ฆฌ ๊ฐœ์„  ํ•„์š”"
659
+ ]
660
+ return {
661
+ "needs_refinement": True,
662
+ "intermediate_steps": steps_delta
663
+ }
664
+
665
+ steps_delta = [
666
+ f"โœ… ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ถฉ๋ถ„ ({result_count}๊ฐœ, ํ‰๊ท  ์ ์ˆ˜: {avg_score:.2f}), ํ•„ํ„ฐ๋ง ๋‹จ๊ณ„๋กœ ์ง„ํ–‰"
667
+ ]
668
+ else:
669
+ # relevance_score๊ฐ€ ์•„์ง ์—†์œผ๋ฉด ๊ฐœ์ˆ˜๋งŒ์œผ๋กœ ํŒ๋‹จ
670
+ steps_delta = [
671
+ f"โœ… ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ถฉ๋ถ„ ({result_count}๊ฐœ), ํ•„ํ„ฐ๋ง ๋‹จ๊ณ„๋กœ ์ง„ํ–‰"
672
+ ]
673
+
674
+ return {
675
+ "needs_refinement": False,
676
+ "intermediate_steps": steps_delta
677
+ }
678
+
679
+
680
+ @trace_node("refine_search")
681
+ def refine_search_node(state: AgentState) -> dict:
682
+ """
683
+ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
684
+
685
+ Open Deep Research ํŒจํ„ด:
686
+ - LLM์ด ์ „๋žต์„ ์„ ํƒ (๊ตฌ์ฒดํ™”/์ผ๋ฐ˜ํ™”/๋ฒˆ์—ญ)
687
+ - ์›๋ณธ ์งˆ๋ฌธ ๋ณด์กด (์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ)
688
+
689
+ LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ:
690
+ - ์ƒํƒœ์— ์›์‹œ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ „๋žต ์ •๋ณด ํฌํ•จ)
691
+ - ํ”„๋กฌํ”„ํŠธ๋Š” ๋…ธ๋“œ ๋‚ด์—์„œ ๋™์  ์ƒ์„ฑ
692
+ """
693
+ user_question = state.user_question
694
+ original_question = state.original_question or user_question
695
+ result_count = len(state.search_results)
696
+
697
+ logger.info("๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ๊ฐœ์„  ์ค‘: %s (%d๊ฐœ ๊ฒฐ๊ณผ)", user_question[:50], result_count)
698
+
699
+ refinement_prompt = f"""๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•˜์„ธ์š”.
700
+
701
+ ์›๋ณธ ์งˆ๋ฌธ: {user_question}
702
+ ํ˜„์žฌ ๊ฒฐ๊ณผ ์ˆ˜: {result_count}๊ฐœ (๋ชฉํ‘œ: 2๊ฐœ ์ด์ƒ)
703
+
704
+ ๊ฐœ์„  ์ „๋žต (ํ•˜๋‚˜ ์„ ํƒ):
705
+ 1. MORE_SPECIFIC: ๊ธฐ์ˆ ์  ์„ธ๋ถ€์‚ฌํ•ญ ์ถ”๊ฐ€
706
+ ์˜ˆ: "React hooks" โ†’ "React useEffect cleanup function dependencies"
707
+
708
+ 2. MORE_GENERAL: ๋” ๋„“์€ ์šฉ์–ด ์‚ฌ์šฉ
709
+ ์˜ˆ: "Spring Cloud Sleuth 2.x trace" โ†’ "distributed tracing Spring Boot"
710
+
711
+ 3. TRANSLATE: ์–ธ์–ด ๋ณ€ํ™˜
712
+ ์˜ˆ: "JWT ์ธ์ฆ ๊ตฌํ˜„" โ†’ "JWT authentication implementation"
713
+ ์˜ˆ: "WebSocket connection" โ†’ "WebSocket ์—ฐ๊ฒฐ ๋ฐฉ๋ฒ•"
714
+
715
+ ๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
716
+ {{
717
+ "new_query": "๊ฐœ์„ ๋œ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ",
718
+ "strategy": "MORE_SPECIFIC|MORE_GENERAL|TRANSLATE",
719
+ "reasoning": "์ด ์ „๋žต์„ ์„ ํƒํ•œ ์ด์œ  1-2๋ฌธ์žฅ"
720
+ }}
721
+
722
+ JSON ์™ธ์— ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋Š” ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”."""
723
+
724
+ try:
725
+ import json
726
+
727
+ messages_to_llm = [HumanMessage(content=refinement_prompt)]
728
+ response = llm.invoke(messages_to_llm)
729
+
730
+ # JSON ํŒŒ์‹ฑ
731
+ response_text = response.content.strip()
732
+
733
+ if "```json" in response_text:
734
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
735
+ elif "```" in response_text:
736
+ response_text = response_text.split("```")[1].split("```")[0].strip()
737
+
738
+ refinement_data = json.loads(response_text)
739
+
740
+ new_query = refinement_data.get("new_query", user_question)
741
+ strategy = refinement_data.get("strategy", "MORE_GENERAL")
742
+ reasoning = refinement_data.get("reasoning", "")
743
+
744
+ steps_delta = [
745
+ f"๐Ÿ”„ ์ฟผ๋ฆฌ ๊ฐœ์„ : {strategy}",
746
+ f" ์ด์ „: {user_question[:50]}...",
747
+ f" ์ดํ›„: {new_query[:50]}...",
748
+ f" ์ด์œ : {reasoning}"
749
+ ]
750
+
751
+ logger.info("์ฟผ๋ฆฌ ๊ฐœ์„  ์™„๋ฃŒ: %s โ†’ %s", user_question[:30], new_query[:30])
752
+
753
+ return {
754
+ "user_question": new_query,
755
+ "original_question": original_question,
756
+ "refinement_count": state.refinement_count + 1,
757
+ "search_results": [], # CRITICAL: ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ œ๊ฑฐ ํ›„ ์žฌ๊ฒ€์ƒ‰
758
+ "intermediate_steps": steps_delta
759
+ }
760
+
761
+ except Exception as e:
762
+ logger.error("์ฟผ๋ฆฌ ๊ฐœ์„  ์‹คํŒจ: %s", e, exc_info=True)
763
+
764
+ # ๊ธฐ๋ณธ ์ „๋žต: ์˜๋ฌธ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ฐ„๋‹จํ•œ fallback)
765
+ fallback_query = user_question + " tutorial example"
766
+
767
+ steps_delta = [
768
+ f"โš ๏ธ ์ฟผ๋ฆฌ ๊ฐœ์„  ์‹คํŒจ, ๊ธฐ๋ณธ ์ „๋žต ์‚ฌ์šฉ",
769
+ f" ์ดํ›„: {fallback_query}"
770
+ ]
771
+
772
+ return {
773
+ "user_question": fallback_query,
774
+ "original_question": original_question,
775
+ "refinement_count": state.refinement_count + 1,
776
+ "search_results": [], # CRITICAL: ์‹คํŒจ ์‹œ์—๋„ ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ œ๊ฑฐ
777
+ "intermediate_steps": steps_delta
778
+ }
779
+
780
+
781
+ @trace_node("filter_and_score")
782
+ def filter_and_score_node(state: AgentState) -> dict:
783
+ """
784
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  ๊ด€๋ จ๋„ ์ ์ˆ˜๋ฅผ ๋งค๊น๋‹ˆ๋‹ค.
785
+
786
+ - ์ตœ์†Œ ๊ธธ์ด 50์ž ์ด์ƒ, URL ์กด์žฌํ•˜๋Š” ๊ฒฐ๊ณผ๋งŒ ์œ ์ง€
787
+ - ์ƒ์œ„ 5๊ฐœ ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด LLM์œผ๋กœ ๊ด€๋ จ๋„ ํ‰๊ฐ€
788
+ - ๊ด€๋ จ๋„ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ์ƒ์œ„ 10๊ฐœ ์„ ํƒ
789
+ """
790
+ search_results = state.search_results
791
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง ์ค‘: %d๊ฐœ", len(search_results))
792
+
793
+ # ๊ธฐ๋ณธ ํ•„ํ„ฐ๋ง
794
+ filtered = [
795
+ r for r in search_results
796
+ if r.content and len(r.content) >= 50 and r.url
797
+ ]
798
+
799
+ logger.info("๊ธฐ๋ณธ ํ•„ํ„ฐ๋ง ํ›„: %d๊ฐœ ๊ฒฐ๊ณผ", len(filtered))
800
+
801
+ # ์ƒ์œ„ 5๊ฐœ ๊ฒฐ๊ณผ๋งŒ LLM์œผ๋กœ ์ ์ˆ˜ ๋งค๊ธฐ๊ธฐ (๋น„์šฉ ์ ˆ๊ฐ)
802
+ for result in filtered[:5]:
803
+ if result.relevance_score is None:
804
+ try:
805
+ scoring_prompt = f"""์งˆ๋ฌธ: {state.user_question}
806
+
807
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: {result.content[:500]}
808
+
809
+ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์งˆ๋ฌธ์— ์–ผ๋งˆ๋‚˜ ๊ด€๋ จ์ด ์žˆ๋Š”์ง€ 0.0์—์„œ 1.0 ์‚ฌ์ด์˜ ์ ์ˆ˜๋กœ ํ‰๊ฐ€ํ•˜์„ธ์š”.
810
+ ์ ์ˆ˜๋งŒ ์ˆซ์ž๋กœ ๋‹ตํ•˜์„ธ์š”. (์˜ˆ: 0.8)"""
811
+
812
+ response = llm.invoke([HumanMessage(content=scoring_prompt)])
813
+ score_str = response.content.strip()
814
+ result.relevance_score = float(score_str)
815
+
816
+ except Exception as e:
817
+ logger.warning("์ ์ˆ˜ ๋งค๊ธฐ๊ธฐ ์‹คํŒจ: %s", e)
818
+ result.relevance_score = 0.5
819
+
820
+ # ๊ด€๋ จ๋„ ์ˆœ์œผ๋กœ ์ •๋ ฌ
821
+ filtered.sort(key=lambda r: r.relevance_score or 0, reverse=True)
822
+
823
+ # ์ƒ์œ„ 5๊ฐœ๋งŒ ์œ ์ง€
824
+ top_results = filtered[:5]
825
+
826
+ subtask_results = dict(state.subtask_results)
827
+ subtask_results["filtered_results"] = [r.model_dump() for r in top_results]
828
+
829
+ steps_delta = [f"โœ‚๏ธ ํ•„ํ„ฐ๋ง ์™„๋ฃŒ: {len(top_results)}๊ฐœ ๊ฒฐ๊ณผ ์„ ํƒ"]
830
+
831
+ logger.info("ํ•„ํ„ฐ๋ง ์™„๋ฃŒ: %d๊ฐœ ๊ฒฐ๊ณผ", len(top_results))
832
+
833
+ return {
834
+ "subtask_results": subtask_results,
835
+ "intermediate_steps": steps_delta
836
+ }
837
+
838
+
839
+ @trace_node("summarize_results")
840
+ def summarize_results_node(state: AgentState) -> dict:
841
+ """
842
+ ํ•„ํ„ฐ๋ง๋œ ๊ฐ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค.
843
+
844
+ ๊ฐ ๊ฒฐ๊ณผ๋ฅผ 2-3๋ฌธ์žฅ์œผ๋กœ ํ•ต์‹ฌ ๋‚ด์šฉ๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
845
+ """
846
+ subtask_results = state.subtask_results
847
+ filtered_results = subtask_results.get("filtered_results", [])
848
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์š”์•ฝ ์ค‘: %d๊ฐœ", len(filtered_results))
849
+
850
+ summaries = []
851
+
852
+ for result_dict in filtered_results:
853
+ try:
854
+ summary_prompt = f"""๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ 2-3๋ฌธ์žฅ์œผ๋กœ ์š”์•ฝํ•˜์„ธ์š”:
855
+
856
+ ์ถœ์ฒ˜: {result_dict['source']}
857
+ ๋‚ด์šฉ: {result_dict['content'][:1000]}
858
+
859
+ ํ•ต์‹ฌ ๋‚ด์šฉ๋งŒ ๊ฐ„๋‹จ๋ช…๋ฃŒํ•˜๊ฒŒ ์š”์•ฝํ•˜์„ธ์š”."""
860
+
861
+ response = llm.invoke([HumanMessage(content=summary_prompt)])
862
+
863
+ summaries.append({
864
+ "source": result_dict['source'],
865
+ "url": result_dict['url'],
866
+ "summary": response.content.strip(),
867
+ "relevance": result_dict.get('relevance_score', 0.5)
868
+ })
869
+
870
+ except Exception as e:
871
+ logger.error("์š”์•ฝ ์‹คํŒจ: %s", e)
872
+
873
+ updated_subtask_results = dict(subtask_results)
874
+ updated_subtask_results["summaries"] = summaries
875
+
876
+ steps_delta = [f"๐Ÿ“ ์š”์•ฝ ์™„๋ฃŒ: {len(summaries)}๊ฐœ ๊ฒฐ๊ณผ"]
877
+
878
+ logger.info("์š”์•ฝ ์™„๋ฃŒ: %d๊ฐœ", len(summaries))
879
+
880
+ return {
881
+ "subtask_results": updated_subtask_results,
882
+ "intermediate_steps": steps_delta
883
+ }
884
+
885
+
886
+ @trace_node("generate_answer")
887
+ async def generate_answer_node(state: AgentState) -> dict:
888
+ """
889
+ ์š”์•ฝ๋œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ข… ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
890
+
891
+ ์˜๋„๋ณ„๋กœ ๋‹ค๋ฅธ ๋‹ต๋ณ€ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ƒ์„ฑ๋œ ๋‹ต๋ณ€์€ ์บ์‹œ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
892
+ """
893
+ subtask_results = state.subtask_results
894
+ summaries = subtask_results.get("summaries", [])
895
+ intent = state.detected_intent or "learning"
896
+
897
+ logger.info("์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์ค‘: %s", intent)
898
+
899
+ # ์˜๋„๋ณ„ ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ
900
+ templates = {
901
+ "debugging": """๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋””๋ฒ„๊น… ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”:
902
+
903
+ ์งˆ๋ฌธ: {question}
904
+
905
+ ์ˆ˜์ง‘๋œ ์ •๋ณด:
906
+ {summaries}
907
+
908
+ ๋‹ต๋ณ€ ๊ตฌ์กฐ:
909
+ 1. ๋ฌธ์ œ ์ •์˜
910
+ 2. ๋ฐœ์ƒ ์›์ธ
911
+ 3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• (์ฝ”๋“œ ์˜ˆ์ œ ํฌํ•จ)
912
+ 4. ์ฃผ์˜์‚ฌํ•ญ
913
+ 5. ์ฐธ๊ณ  ์ž๋ฃŒ
914
+
915
+ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ Markdown ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.""",
916
+
917
+ "learning": """๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•™์Šต ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”:
918
+
919
+ ์งˆ๋ฌธ: {question}
920
+
921
+ ์ˆ˜์ง‘๋œ ์ •๋ณด:
922
+ {summaries}
923
+
924
+ ๋‹ต๋ณ€ ๊ตฌ์กฐ:
925
+ 1. ๊ฐœ๋… ์„ค๋ช… (๊ฐ„๋‹จ๋ช…๋ฃŒ)
926
+ 2. ๋™์ž‘ ์›๋ฆฌ
927
+ 3. ์˜ˆ์ œ ์ฝ”๋“œ (์ฃผ์„ ํฌํ•จ)
928
+ 4. ์‹ค๋ฌด ํ™œ์šฉ ํŒ
929
+ 5. ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ
930
+
931
+ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ Markdown ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.""",
932
+
933
+ "code_review": """๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”:
934
+
935
+ ์งˆ๋ฌธ: {question}
936
+
937
+ ์ˆ˜์ง‘๋œ ์ •๋ณด:
938
+ {summaries}
939
+
940
+ ๋‹ต๋ณ€ ๊ตฌ์กฐ:
941
+ 1. ํ˜„์žฌ ์ ‘๊ทผ ๋ฐฉ์‹ ๋ถ„์„
942
+ 2. ๊ฐœ์„  ํฌ์ธํŠธ
943
+ 3. ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์ œ
944
+ 4. ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
945
+ 5. ์ฐธ๊ณ  ํŒจํ„ด
946
+
947
+ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ Markdown ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”."""
948
+ }
949
+
950
+ template = templates.get(intent, templates["learning"])
951
+
952
+ # ์š”์•ฝ ํ…์ŠคํŠธ ํฌ๋งทํŒ…
953
+ summaries_text = "\n\n".join([
954
+ f"์ถœ์ฒ˜: {s['source']} ({s['url']})\n์š”์•ฝ: {s['summary']}"
955
+ for s in summaries
956
+ ])
957
+
958
+ # ์ด์ „ ๋Œ€ํ™” ๋งฅ๋ฝ ์ถ”๊ฐ€ (messages ์‚ฌ์šฉ)
959
+ context_prefix = ""
960
+ messages_history = state.messages
961
+ if messages_history and len(messages_history) > 1:
962
+ context_prefix = "์ด์ „ ๋Œ€๏ฟฝ๏ฟฝ๏ฟฝ ๋งฅ๋ฝ:\n"
963
+ # ์ตœ๊ทผ 6๊ฐœ ๋ฉ”์‹œ์ง€ (3ํ„ด) ์‚ฌ์šฉ
964
+ for msg in messages_history[-6:]:
965
+ if hasattr(msg, 'type'):
966
+ if msg.type == "human":
967
+ context_prefix += f"์‚ฌ์šฉ์ž: {msg.content}\n"
968
+ elif msg.type == "ai":
969
+ context_prefix += f"AI: {msg.content[:200]}...\n\n"
970
+ context_prefix += "---\nํ˜„์žฌ ์งˆ๋ฌธ:\n"
971
+
972
+ final_prompt = (context_prefix + template).format(
973
+ question=(state.original_question or state.user_question),
974
+ summaries=summaries_text
975
+ )
976
+
977
+ updates = {}
978
+ steps_delta: List[str] = []
979
+
980
+ try:
981
+ response = llm.invoke([HumanMessage(content=final_prompt)])
982
+ final_answer = response.content.strip()
983
+
984
+ updates["final_answer"] = final_answer
985
+
986
+ # Phase 3: ์กฐ๊ฑด๋ถ€ ์บ์‹œ ์ €์žฅ
987
+ # - clarification: ์บ์‹œ ๊ธˆ์ง€ (๊ทธ๋ž˜ํ”„ ์ƒ generate_with_history๋กœ ๋น ์ง€์ง€๋งŒ, ๋ฐฉ์–ด์ ์œผ๋กœ ํ•œ ๋ฒˆ ๋” ์ฒดํฌ)
988
+ # - new_topic/independent: ์บ์‹œ ๊ฐ€๋Šฅ(should_cache๊ฐ€ True์ผ ๋•Œ)
989
+ should_cache = state.should_cache if state.should_cache is not None else True
990
+ canonical_question = state.canonical_question
991
+ qtype = state.question_type or "independent"
992
+
993
+ if should_cache and qtype in ["new_topic", "independent"]:
994
+ # ์บ์‹œํ•  ์งˆ๋ฌธ: canonical_question ์šฐ์„ , ์—†์œผ๋ฉด ์›๋ณธ ์งˆ๋ฌธ
995
+ question_to_cache = canonical_question or state.user_question
996
+
997
+ await qdrant_manager.save_to_cache(
998
+ question=question_to_cache,
999
+ answer=final_answer
1000
+ )
1001
+
1002
+ steps_delta.append(f"โœ… ์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(final_answer)}์ž)")
1003
+ steps_delta.append(f"๐Ÿ’พ ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ (์งˆ๋ฌธ: {question_to_cache[:50]}...)")
1004
+ logger.info("์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ๋ฐ ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ: %s", question_to_cache[:50])
1005
+ else:
1006
+ steps_delta.append(f"โœ… ์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(final_answer)}์ž)")
1007
+ steps_delta.append("โš ๏ธ ์บ์‹œ ์ €์žฅ ์ƒ๋žต (๋…๋ฆฝ์ ์ด์ง€ ์•Š๊ฑฐ๋‚˜ ์ผํšŒ์„ฑ ์งˆ๋ฌธ)")
1008
+ logger.info("์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ (์บ์‹œ ์ €์žฅ ์ƒ๋žต)")
1009
+
1010
+ except Exception as e:
1011
+ logger.error("๋‹ต๋ณ€ ์ƒ์„ฑ ์‹คํŒจ: %s", e, exc_info=True)
1012
+ updates["final_answer"] = "๋‹ต๋ณ€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”."
1013
+ steps_delta.append(f"โŒ ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹คํŒจ: {str(e)}")
1014
+
1015
+ updates["intermediate_steps"] = steps_delta
1016
+
1017
+ # Phase 4: Multi-question handling
1018
+ # NOTE: AgentState๋Š” Pydantic(BaseModel)์ด๋ฏ€๋กœ dict-style state.get(...) ์‚ฌ์šฉ ๊ธˆ์ง€
1019
+ if state.is_multi_question:
1020
+ answer_text = updates.get("final_answer")
1021
+ if answer_text:
1022
+ # Append to multi_answers (reducer will auto-merge)
1023
+ updates["multi_answers"] = [{
1024
+ "index": state.sub_question_index,
1025
+ "question": state.sub_question_text or state.user_question,
1026
+ "answer": answer_text
1027
+ }]
1028
+ logger.info("๋‹ค์ค‘ ์งˆ๋ฌธ ๋‹ต๋ณ€ ์ถ”๊ฐ€: Q%d", state.sub_question_index)
1029
+
1030
+ return updates
1031
+
1032
+
1033
+ @trace_node("return_cached_answer")
1034
+ def return_cached_answer_node(state: AgentState) -> dict:
1035
+ """
1036
+ ์บ์‹œ ํžˆํŠธ ์‹œ ์ €์žฅ๋œ ๋‹ต๋ณ€์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
1037
+
1038
+ ๊ฒ€์ƒ‰ ๋ฐ ์ƒ์„ฑ ๊ณผ์ •์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์ฆ‰์‹œ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
1039
+ """
1040
+ logger.info("์บ์‹œ๋œ ๋‹ต๋ณ€ ๋ฐ˜ํ™˜")
1041
+
1042
+ steps_delta = ["๐Ÿ’พ ์บ์‹œ๋œ ๋‹ต๋ณ€ ๋ฐ˜ํ™˜ (๊ฒ€์ƒ‰ ์ƒ๋žต)"]
1043
+
1044
+ return {
1045
+ "final_answer": state.cached_result,
1046
+ "intermediate_steps": steps_delta
1047
+ }
1048
+
1049
+
1050
+ @trace_node("handle_too_many_questions")
1051
+ def handle_too_many_questions_node(state: AgentState) -> dict:
1052
+ """
1053
+ 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
1054
+
1055
+ ๋Œ€ํ™”๋ฅผ ์ข…๋ฃŒํ•˜์ง€ ์•Š๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์‹œ ์งˆ๋ฌธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
1056
+ """
1057
+ plan = state.plan or {}
1058
+ error_message = plan.get("error_message", "")
1059
+ sub_questions = plan.get("sub_questions", [])
1060
+
1061
+ logger.info("์งˆ๋ฌธ ์ˆ˜ ์ดˆ๊ณผ: %d๊ฐœ", len(sub_questions))
1062
+
1063
+ default_message = """์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 2๊ฐœ์˜ ์งˆ๋ฌธ๊นŒ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1064
+
1065
+ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์„œ ๋‹ค์‹œ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”:
1066
+
1067
+ 1. **ํ•˜๋‚˜์˜ ์ฃผ์ œ๋กœ ํ†ตํ•ฉํ•ด์„œ ์งˆ๋ฌธ**
1068
+ ์˜ˆ: "JWT ์ธ์ฆ๊ณผ CORS ์„ค์ •์„ ํ•จ๊ป˜ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•"
1069
+
1070
+ 2. **๊ฐ€์žฅ ์ค‘์š”ํ•œ 2๊ฐœ ์งˆ๋ฌธ๋งŒ ์„ ํƒ**
1071
+ ์˜ˆ: "JWT๊ฐ€ ๋ญ์•ผ? ๋‚ด ์ฝ”๋“œ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•ด?"
1072
+
1073
+ 3. **์งˆ๋ฌธ์„ ๋‚˜๋ˆ ์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์งˆ๋ฌธ**
1074
+ ์˜ˆ: ๋จผ์ € "JWT๊ฐ€ ๋ญ์•ผ?" ์งˆ๋ฌธ โ†’ ๋‹ต๋ณ€ ํ™•์ธ โ†’ ๋‹ค์Œ ์งˆ๋ฌธ
1075
+
1076
+ ์–ด๋–ป๊ฒŒ ๋„์™€๋“œ๋ฆด๊นŒ์š”?"""
1077
+
1078
+ final_message = error_message if error_message else default_message
1079
+
1080
+ steps_delta = [
1081
+ f"โš ๏ธ ์งˆ๋ฌธ ์ˆ˜ ์ดˆ๊ณผ: {len(sub_questions)}๊ฐœ",
1082
+ "๐Ÿ’ฌ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ œ๊ณต (๋Œ€ํ™” ๊ณ„์† ๊ฐ€๋Šฅ)"
1083
+ ]
1084
+
1085
+ return {
1086
+ "final_answer": final_message,
1087
+ "intermediate_steps": steps_delta
1088
+ }
1089
+
1090
+
1091
+ @trace_node("initiate_dynamic_search")
1092
+ def initiate_dynamic_search_node(state: AgentState) -> dict:
1093
+ """
1094
+ ๋‹ค์ค‘ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ์˜ ์ง„์ž… ๋…ธ๋“œ.
1095
+
1096
+ IMPORTANT:
1097
+ - LangGraph์—์„œ `List[Send]`๋Š” **๋…ธ๋“œ ๋ฐ˜ํ™˜๊ฐ’**์ด ์•„๋‹ˆ๋ผ,
1098
+ `add_conditional_edges(...)`์— ์ „๋‹ฌํ•˜๋Š” **edge ํ•จ์ˆ˜ ๋ฐ˜ํ™˜๊ฐ’**์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
1099
+ - ๋”ฐ๋ผ์„œ ์ด ๋…ธ๋“œ๋Š” dict ์—…๋ฐ์ดํŠธ๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ณ ,
1100
+ ์‹ค์ œ fan-out์€ ๋ณ„๋„ edge ํ•จ์ˆ˜(`fanout_multi_questions`)๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
1101
+ """
1102
+ plan = state.plan or {}
1103
+ sub_questions = plan.get("sub_questions", [])
1104
+ logger.info("๋™์  ๋ณต์ œ ์ค€๋น„: %d๊ฐœ ์งˆ๋ฌธ", len(sub_questions))
1105
+ return {
1106
+ "intermediate_steps": [f"๐Ÿ”€ ๋‹ค์ค‘ ์งˆ๋ฌธ fan-out ์ค€๋น„: {len(sub_questions)}๊ฐœ"]
1107
+ }
1108
+
1109
+
1110
+ def fanout_multi_questions(state: AgentState):
1111
+ """
1112
+ ๋‹ค์ค‘ ์งˆ๋ฌธ์„ Send API๋กœ fan-out ํ•ฉ๋‹ˆ๋‹ค.
1113
+
1114
+ ๋ฐ˜ํ™˜๊ฐ’(List[Send])์€ conditional edge ํ•จ์ˆ˜์—์„œ๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.
1115
+ """
1116
+ from langgraph.types import Send
1117
+
1118
+ plan = state.plan or {}
1119
+ sub_questions = plan.get("sub_questions", [])
1120
+ original_question = state.user_question
1121
+ messages = state.messages
1122
+
1123
+ logger.info("๋™์  ๋ณต์ œ: %d๊ฐœ ์งˆ๋ฌธ์„ ๊ฐ๊ฐ ์ „์ฒด ๊ทธ๋ž˜ํ”„๋กœ ์‹คํ–‰", len(sub_questions))
1124
+
1125
+ sends = []
1126
+ for i, sq in enumerate(sub_questions):
1127
+ # IMPORTANT: ์ด ํ”„๋กœ์ ํŠธ๋Š” AgentState(BaseModel)๋ฅผ ๋…ธ๋“œ ์ž…๋ ฅ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ,
1128
+ # Send arg๋„ dict๊ฐ€ ์•„๋‹ˆ๋ผ AgentState ์ธ์Šคํ„ด์Šค๋กœ ๋ณด๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
1129
+ child = state.model_copy(deep=True)
1130
+
1131
+ # ์งˆ๋ฌธ ๊ต์ฒด + ๋‹ค์ค‘ ์งˆ๋ฌธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
1132
+ child.user_question = sq
1133
+ child.is_multi_question = True
1134
+ child.sub_question_index = i
1135
+ child.sub_question_text = sq
1136
+ child.original_multi_question = original_question
1137
+
1138
+ # ๊ณตํ†ต ์œ ์ง€ ํ•„๋“œ
1139
+ child.messages = messages
1140
+ child.plan = plan
1141
+
1142
+ # ๊ธฐ์กด ๊ทธ๋ž˜ํ”„๊ฐ€ ๋‹ค์‹œ ์ฑ„์šธ ํ•„๋“œ๋“ค์€ ์ดˆ๊ธฐํ™”
1143
+ child.question_type = None
1144
+ child.should_cache = None
1145
+ child.canonical_question = None
1146
+ child.analysis_reasoning = None
1147
+ child.cached_result = None
1148
+ child.detected_intent = None
1149
+ child.search_results = []
1150
+ child.subtask_results = {}
1151
+ child.refinement_count = 0
1152
+ child.needs_refinement = False
1153
+ child.original_question = None
1154
+ child.final_answer = None
1155
+ child.multi_answers = []
1156
+ child.intermediate_steps = [f"๐Ÿ”„ ์งˆ๋ฌธ {i+1}/{len(sub_questions)}: {sq[:50]}"]
1157
+
1158
+ # ๋‹ค์ค‘ ์งˆ๋ฌธ์€ outer graph์—์„œ ๊ธฐ์กด ํŒŒ์ดํ”„๋ผ์ธ ์ „์ฒด๋ฅผ ๋ณ‘๋ ฌ๋กœ ๋Œ๋ฆฌ๋ฉด
1159
+ # scalar state ์ฑ„๋„(question_type ๋“ฑ)์—์„œ concurrent update ์ถฉ๋Œ์ด ๋‚ฉ๋‹ˆ๋‹ค.
1160
+ # ๋”ฐ๋ผ์„œ worker ๋…ธ๋“œ ์•ˆ์—์„œ '๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„'๋ฅผ ๋ณ„๋„๋กœ ์‹คํ–‰ํ•œ ๋’ค,
1161
+ # outer state์—๋Š” multi_answers(reducer)๋งŒ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
1162
+ sends.append(Send("run_single_question_worker", child))
1163
+
1164
+ return sends
1165
+
1166
+
1167
+ @trace_node("combine_answers")
1168
+ def combine_answers_node(state: AgentState) -> dict:
1169
+ """
1170
+ Fan-in: ๋ชจ๋“  Send๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด multi_answers๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.
1171
+
1172
+ Reducer (Annotated[List[dict], add])๊ฐ€ ์ž๋™์œผ๋กœ
1173
+ ๋ชจ๋“  parallel Send์˜ ๊ฒฐ๊ณผ๋ฅผ multi_answers์— ๋ชจ์•„๋‘ก๋‹ˆ๋‹ค.
1174
+
1175
+ ์ด ๋…ธ๋“œ๋Š” ๋‹จ์ˆœํžˆ ๋ชจ์•„์ง„ ๊ฒฐ๊ณผ๋ฅผ ์ฝ์–ด์„œ Markdown์œผ๋กœ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.
1176
+ """
1177
+ answers = state.multi_answers
1178
+ original_question = state.original_multi_question or state.user_question
1179
+
1180
+ if not answers:
1181
+ logger.error("๋‹ค์ค‘ ๋‹ต๋ณ€์ด ๋น„์–ด์žˆ์Œ")
1182
+ return {
1183
+ "final_answer": "๋‹ต๋ณ€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.",
1184
+ "intermediate_steps": ["โŒ multi_answers ๋น„์–ด์žˆ์Œ"]
1185
+ }
1186
+
1187
+ # ์ธ๋ฑ์Šค ์ˆœ์œผ๋กœ ์ •๋ ฌ
1188
+ answers.sort(key=lambda x: x["index"])
1189
+
1190
+ # Markdown ํ˜•์‹์œผ๋กœ ์กฐํ•ฉ
1191
+ combined_parts = []
1192
+ for ans in answers:
1193
+ section = f"""## {ans['index']+1}. {ans['question']}
1194
+
1195
+ {ans['answer']}"""
1196
+ combined_parts.append(section)
1197
+
1198
+ combined = "\n\n---\n\n".join(combined_parts)
1199
+
1200
+ # ํ—ค๋” ์ถ”๊ฐ€
1201
+ header = f"# ๋‹ค์ค‘ ์งˆ๋ฌธ ๋‹ต๋ณ€\n\n์›๋ณธ ์งˆ๋ฌธ: {original_question}\n\n---\n\n"
1202
+ final_combined = header + combined
1203
+
1204
+ logger.info("๋‹ค์ค‘ ๋‹ต๋ณ€ ์กฐํ•ฉ ์™„๋ฃŒ: %d๊ฐœ", len(answers))
1205
+
1206
+ return {
1207
+ "final_answer": final_combined,
1208
+ "intermediate_steps": [f"โœ… {len(answers)}๊ฐœ ๋‹ต๋ณ€ ์กฐํ•ฉ ์™„๋ฃŒ"]
1209
+ }
1210
+
1211
+
1212
+ def _build_search_subgraph_local() -> StateGraph:
1213
+ """nodes.py ๋‚ด๋ถ€์—์„œ ๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„์šฉ ๊ฒ€์ƒ‰ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์„ฑ."""
1214
+ subgraph = StateGraph(AgentState)
1215
+ subgraph.add_node("filter_and_score", filter_and_score_node)
1216
+ subgraph.add_node("summarize_results", summarize_results_node)
1217
+ subgraph.add_edge(START, "filter_and_score")
1218
+ subgraph.add_edge("filter_and_score", "summarize_results")
1219
+ subgraph.add_edge("summarize_results", END)
1220
+ return subgraph.compile()
1221
+
1222
+
1223
+ def _get_single_question_agent():
1224
+ """
1225
+ ๋‹ค์ค‘ ์งˆ๋ฌธ worker์—์„œ ์‚ฌ์šฉํ•  '๋‹จ์ผ ์งˆ๋ฌธ ํŒŒ์ดํ”„๋ผ์ธ' ๊ทธ๋ž˜ํ”„๋ฅผ lazy-compile ํ•ด์„œ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
1226
+ (outer state ์ถฉ๋Œ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด, worker ๋‚ด๋ถ€์—์„œ ๋ณ„๋„ ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰)
1227
+ """
1228
+ global _SINGLE_QUESTION_AGENT # type: ignore[name-defined]
1229
+ try:
1230
+ return _SINGLE_QUESTION_AGENT # type: ignore[name-defined]
1231
+ except Exception:
1232
+ pass
1233
+
1234
+ # ---- routing helpers (graph.py ์˜ ๋‹จ์ผ ์งˆ๋ฌธ ํ๋ฆ„๊ณผ ๋™์ผ) ----
1235
+ def _route_after_analysis(s: AgentState) -> Literal["generate_with_history", "check_cache"]:
1236
+ raw_qtype = s.question_type or "independent"
1237
+ legacy_map = {"followup": "clarification", "cache_candidate": "independent", "new_search": "independent"}
1238
+ question_type = legacy_map.get(raw_qtype, raw_qtype)
1239
+ return "generate_with_history" if question_type == "clarification" else "check_cache"
1240
+
1241
+ def _route_after_cache(s: AgentState) -> Literal["return_cached_answer", "classify_intent"]:
1242
+ return "return_cached_answer" if s.cached_result else "classify_intent"
1243
+
1244
+ def _route_after_evaluation(s: AgentState) -> Literal["refine_search", "search_subgraph"]:
1245
+ if s.needs_refinement and s.refinement_count < 1:
1246
+ return "refine_search"
1247
+ return "search_subgraph"
1248
+
1249
+ def _initiate_parallel_search(s: AgentState):
1250
+ return [
1251
+ Send("search_stackoverflow", s),
1252
+ Send("search_github", s),
1253
+ Send("search_official_docs", s),
1254
+ ]
1255
+
1256
+ # ---- build ----
1257
+ g = StateGraph(AgentState)
1258
+ g.add_node("analyze_question", analyze_question_node)
1259
+ g.add_node("generate_with_history", generate_with_history_node)
1260
+ g.add_node("check_cache", check_cache_node)
1261
+ g.add_node("return_cached_answer", return_cached_answer_node)
1262
+ g.add_node("classify_intent", classify_intent_node)
1263
+ g.add_node("search_stackoverflow", search_stackoverflow_node)
1264
+ g.add_node("search_github", search_github_node)
1265
+ g.add_node("search_official_docs", search_official_docs_node)
1266
+ g.add_node("collect_results", collect_results_node)
1267
+ g.add_node("evaluate_results", evaluate_results_node)
1268
+ g.add_node("refine_search", refine_search_node)
1269
+ g.add_node("generate_answer", generate_answer_node)
1270
+
1271
+ search_subgraph = _build_search_subgraph_local()
1272
+ g.add_node("search_subgraph", search_subgraph)
1273
+
1274
+ g.add_edge(START, "analyze_question")
1275
+ g.add_conditional_edges(
1276
+ "analyze_question",
1277
+ _route_after_analysis,
1278
+ {"generate_with_history": "generate_with_history", "check_cache": "check_cache"},
1279
+ )
1280
+ g.add_edge("generate_with_history", END)
1281
+ g.add_conditional_edges(
1282
+ "check_cache",
1283
+ _route_after_cache,
1284
+ {"return_cached_answer": "return_cached_answer", "classify_intent": "classify_intent"},
1285
+ )
1286
+ g.add_edge("return_cached_answer", END)
1287
+ g.add_conditional_edges("classify_intent", _initiate_parallel_search)
1288
+ g.add_edge("search_stackoverflow", "collect_results")
1289
+ g.add_edge("search_github", "collect_results")
1290
+ g.add_edge("search_official_docs", "collect_results")
1291
+ g.add_edge("collect_results", "evaluate_results")
1292
+ g.add_conditional_edges(
1293
+ "evaluate_results",
1294
+ _route_after_evaluation,
1295
+ {"refine_search": "refine_search", "search_subgraph": "search_subgraph"},
1296
+ )
1297
+ g.add_edge("refine_search", "classify_intent")
1298
+ g.add_edge("search_subgraph", "generate_answer")
1299
+ g.add_edge("generate_answer", END)
1300
+
1301
+ _SINGLE_QUESTION_AGENT = g.compile()
1302
+ return _SINGLE_QUESTION_AGENT
1303
+
1304
+
1305
+ @trace_node("run_single_question_worker")
1306
+ async def run_single_question_worker_node(state: AgentState) -> dict:
1307
+ """
1308
+ ๋‹ค์ค‘ ์งˆ๋ฌธ์˜ ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ์„ '๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„'๋กœ ์‹คํ–‰ํ•œ ๋’ค,
1309
+ outer graph์—๋Š” reducer ์ฑ„๋„(multi_answers)๋งŒ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
1310
+ """
1311
+ agent = _get_single_question_agent()
1312
+
1313
+ # inner ์‹คํ–‰์€ multi-question ํ”Œ๋ž˜๊ทธ๋ฅผ ๊บผ์„œ(=multi_answers append ๋ฐฉ์ง€)
1314
+ inner = state.model_copy(deep=True)
1315
+ inner.is_multi_question = False
1316
+ inner.multi_answers = []
1317
+
1318
+ result = await agent.ainvoke(
1319
+ {
1320
+ "user_question": inner.user_question,
1321
+ "messages": inner.messages,
1322
+ }
1323
+ )
1324
+
1325
+ answer_text = result.get("final_answer") or ""
1326
+ return {
1327
+ "multi_answers": [
1328
+ {
1329
+ "index": state.sub_question_index,
1330
+ "question": state.sub_question_text or state.user_question,
1331
+ "answer": answer_text,
1332
+ }
1333
+ ],
1334
+ "intermediate_steps": [f"โœ… ์„œ๋ธŒ ์งˆ๋ฌธ {state.sub_question_index + 1} ์ฒ˜๋ฆฌ ์™„๋ฃŒ"],
1335
+ }
1336
+
1337
+
1338
+ @trace_node("generate_with_history")
1339
+ async def generate_with_history_node(state: AgentState) -> dict:
1340
+ """
1341
+ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๋งŒ ์‚ฌ์šฉํ•˜์—ฌ ํ›„์† ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.
1342
+
1343
+ Phase 2: Follow-up Handler
1344
+ - ์บ์‹œ ๊ฒ€์ƒ‰ ์•ˆ ํ•จ
1345
+ - ์›น ๊ฒ€์ƒ‰ ์•ˆ ํ•จ
1346
+ - ์บ์‹œ์— ์ €์žฅ ์•ˆ ํ•จ
1347
+ - messages ํžˆ์Šคํ† ๋ฆฌ๋งŒ ํ™œ์šฉ
1348
+ """
1349
+ user_question = state.user_question
1350
+ messages_history = state.messages
1351
+
1352
+ logger.info("๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ: %s", user_question[:50])
1353
+
1354
+ # ๋Œ€ํ™” ๋งฅ๋ฝ ๊ตฌ์„ฑ
1355
+ context_prompt = "์ด์ „ ๋Œ€ํ™”๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ํ›„์† ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”.\n\n"
1356
+
1357
+ if messages_history:
1358
+ context_prompt += "๋Œ€ํ™” ๋‚ด์—ญ:\n"
1359
+ for msg in messages_history[:-1]: # ํ˜„์žฌ ์งˆ๋ฌธ ์ œ์™ธ
1360
+ if hasattr(msg, 'type') and hasattr(msg, 'content'):
1361
+ role = "์‚ฌ์šฉ์ž" if msg.type == "human" else "AI"
1362
+ context_prompt += f"{role}: {msg.content}\n\n"
1363
+
1364
+ context_prompt += f"ํ˜„์žฌ ์งˆ๋ฌธ: {user_question}\n\n"
1365
+ context_prompt += "์ด์ „ ๋Œ€ํ™” ๋งฅ๋ฝ์„ ๊ณ ๋ คํ•˜์—ฌ ์ž์„ธํ•˜๊ณ  ์นœ์ ˆํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”."
1366
+
1367
+ updates = {}
1368
+ steps_delta: List[str] = []
1369
+
1370
+ try:
1371
+ response = llm.invoke([HumanMessage(content=context_prompt)])
1372
+ final_answer = response.content.strip()
1373
+
1374
+ updates["final_answer"] = final_answer
1375
+ steps_delta.append(f"๐Ÿ’ฌ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ (๊ธธ์ด: {len(final_answer)}์ž)")
1376
+ steps_delta.append("โš ๏ธ ์บ์‹œ ์ €์žฅ ์ƒ๋žต (๋ณด์ถฉ ์š”์ฒญ)")
1377
+
1378
+ logger.info("๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ")
1379
+
1380
+ except Exception as e:
1381
+ logger.error("๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹คํŒจ: %s", e, exc_info=True)
1382
+ updates["final_answer"] = "๋‹ต๋ณ€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”."
1383
+ steps_delta.append(f"โŒ ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹คํŒจ: {str(e)}")
1384
+
1385
+ updates["intermediate_steps"] = steps_delta
1386
+ return updates
1387
+
CodeWeaver/src/agent/state.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
15
+ intermediate_steps reducer.
16
+
17
+ - ๊ธฐ๋ณธ ๋™์ž‘: old + new (๋ณ‘๋ ฌ ๋…ธ๋“œ์—์„œ ๋™์‹œ์— step์„ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ)
18
+ - ๋ฆฌ์…‹ ๋™์ž‘: new์˜ ์ฒซ ์›์†Œ๊ฐ€ _STEPS_RESET_TOKEN ์ด๋ฉด old๋ฅผ ๋ฒ„๋ฆฌ๊ณ  new[1:]๋กœ ๊ต์ฒด
19
+ (์ฒดํฌํฌ์ธํŒ…์œผ๋กœ ๋ˆ„์ ๋œ step์„ '์ด๋ฒˆ ์‹คํ–‰(run)' ๊ธฐ์ค€์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•จ)
20
+ """
21
+ if not new:
22
+ return old
23
+ if new[0] == _STEPS_RESET_TOKEN:
24
+ return new[1:]
25
+ return old + new
26
+
27
+
28
+ def merge_multi_answers(old: List[Dict[str, Any]], new: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
29
+ """
30
+ multi_answers reducer.
31
+
32
+ - ๊ธฐ๋ณธ ๋™์ž‘: old + new (๋ณ‘๋ ฌ worker์—์„œ ๋‹ต๋ณ€์„ ๋™์‹œ์— append ๊ฐ€๋Šฅ)
33
+ - ๋ฆฌ์…‹ ๋™์ž‘: new์˜ ์ฒซ ์›์†Œ๊ฐ€ {"__token__": _MULTI_ANS_RESET_TOKEN} ์ด๋ฉด
34
+ old๋ฅผ ๋ฒ„๋ฆฌ๊ณ  new[1:]๋กœ ๊ต์ฒด
35
+ (์ฒดํฌํฌ์ธํŒ…/์Šค๋ ˆ๋“œ ์œ ์ง€๋กœ ์ธํ•ด ์ด์ „ ํ„ด์˜ multi_answers๊ฐ€ ๋ˆ„์ ๋˜๋Š” ๋ฌธ์ œ ๋ฐฉ์ง€)
36
+ """
37
+ if not new:
38
+ return old
39
+ head = new[0]
40
+ if isinstance(head, dict) and head.get("__token__") == _MULTI_ANS_RESET_TOKEN:
41
+ return new[1:]
42
+ return old + new
43
+
44
+
45
+ class SearchResult(BaseModel):
46
+ """๊ฒ€์ƒ‰ ๋„๋ฉ”์ธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋‹จ์ผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชจ๋ธ."""
47
+
48
+ source: str = Field(
49
+ ...,
50
+ description="๊ฒ€์ƒ‰ ์ถœ์ฒ˜ (์˜ˆ: Stack Overflow, ๊ณต์‹ ๋ฌธ์„œ, GitHub Issues ๋“ฑ)",
51
+ )
52
+ content: str = Field(
53
+ ...,
54
+ description="๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ํ•ต์‹ฌ ๋‚ด์šฉ ๋˜๋Š” ๋ฐœ์ทŒ ํ…์ŠคํŠธ",
55
+ )
56
+ url: Optional[str] = Field(
57
+ default=None,
58
+ description="๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ์›๋ณธ ์ถœ์ฒ˜ URL (์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์„ค์ •)",
59
+ )
60
+ relevance_score: Optional[float] = Field(
61
+ default=None,
62
+ description="๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ์™€์˜ ๊ด€๋ จ๋„ ์ ์ˆ˜ (0.0โ€“1.0 ๋ฒ”์œ„, ํด์ˆ˜๋ก ๋” ๊ด€๋ จ ์žˆ์Œ)",
63
+ )
64
+
65
+
66
+ class AgentState(BaseModel):
67
+ """CodeWeaver LangGraph ์—์ด์ „ํŠธ์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Pydantic ๋ชจ๋ธ.
68
+
69
+ LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ:
70
+ - Pydantic BaseModel ์‚ฌ์šฉ (ํƒ€์ž… ์•ˆ์ „์„ฑ)
71
+ - messages ํ•„๋“œ์— add_messages reducer ์ ์šฉ
72
+ - ๋ชจ๋“  ํ•„๋“œ์— ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต
73
+ """
74
+
75
+ # Core fields
76
+ user_question: str = Field(default="", description="์‚ฌ์šฉ์ž์˜ ์›๋ณธ ์งˆ๋ฌธ")
77
+ messages: Annotated[List[BaseMessage], add_messages] = Field(
78
+ default_factory=list,
79
+ description="๋Œ€ํ™” ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ (add_messages reducer ์‚ฌ์šฉ)"
80
+ )
81
+
82
+ # Legacy conversation history (์œ ์ง€ํ•˜๋˜ messages ์šฐ์„ )
83
+ conversation_history: Optional[List[Tuple[str, str]]] = Field(
84
+ default=None,
85
+ description="๋ ˆ๊ฑฐ์‹œ ๋Œ€ํ™” ๋‚ด์—ญ (messages ์šฐ์„  ์‚ฌ์šฉ)"
86
+ )
87
+
88
+ # Intent classification
89
+ detected_intent: Optional[Literal["debugging", "learning", "code_review"]] = Field(
90
+ default=None,
91
+ description="๋ถ„๋ฅ˜๋œ ์งˆ๋ฌธ ์˜๋„"
92
+ )
93
+
94
+ # Cache-related
95
+ cached_result: Optional[str] = Field(
96
+ default=None,
97
+ description="๋ฒกํ„ฐ DB ์บ์‹œ์—์„œ ์กฐํšŒ๋œ ๋‹ต๋ณ€"
98
+ )
99
+
100
+ # Search results (Send API๋ฅผ ์œ„ํ•œ reducer ์‚ฌ์šฉ)
101
+ search_results: Annotated[List[SearchResult], add] = Field(
102
+ default_factory=list,
103
+ description="๋ณ‘๋ ฌ ๊ฒ€์ƒ‰์œผ๋กœ ์ˆ˜์ง‘๋œ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ (Send API๋กœ ๋ณ‘๋ ฌ ์—…๋ฐ์ดํŠธ)"
104
+ )
105
+
106
+ # Intermediate processing
107
+ subtask_results: Dict[str, Any] = Field(
108
+ default_factory=dict,
109
+ description="์„œ๋ธŒํƒœ์Šคํฌ ์‹คํ–‰ ๊ฒฐ๊ณผ ์ €์žฅ์†Œ"
110
+ )
111
+
112
+ # Final output
113
+ final_answer: Optional[str] = Field(
114
+ default=None,
115
+ description="์ตœ์ข… ์ƒ์„ฑ๋œ ๋‹ต๋ณ€"
116
+ )
117
+
118
+ # Debugging/tracing (๋ณ‘๋ ฌ ๋…ธ๋“œ + ์‹คํ–‰ ๋‹จ์œ„ ๋ฆฌ์…‹ ์ง€์› reducer ์‚ฌ์šฉ)
119
+ intermediate_steps: Annotated[List[str], merge_intermediate_steps] = Field(
120
+ default_factory=list,
121
+ description="์‹คํ–‰ ๋‹จ๊ณ„๋ณ„ ๋กœ๊ทธ (๋ณ‘๋ ฌ ๋…ธ๋“œ์—์„œ ๋™์‹œ ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ)"
122
+ )
123
+
124
+ # Question analysis & cache eligibility
125
+ question_type: Optional[Literal["clarification", "new_topic", "independent"]] = Field(
126
+ default=None,
127
+ description="์งˆ๋ฌธ ์œ ํ˜• ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ"
128
+ )
129
+ analysis_reasoning: Optional[str] = Field(
130
+ default=None,
131
+ description="์งˆ๋ฌธ ๋ถ„์„ ์ด์œ "
132
+ )
133
+ should_cache: Optional[bool] = Field(
134
+ default=None,
135
+ description="์บ์‹œ ์ €์žฅ ์—ฌ๋ถ€"
136
+ )
137
+ canonical_question: Optional[str] = Field(
138
+ default=None,
139
+ description="์ •๊ทœํ™”๋œ ์งˆ๋ฌธ (์บ์‹œ์šฉ)"
140
+ )
141
+
142
+ # Planning & Refinement (Phase 3: Open Deep Research pattern)
143
+ plan: Optional[Dict[str, Any]] = Field(
144
+ default=None,
145
+ description="์งˆ๋ฌธ ๋ถ„ํ•ด ๊ณ„ํš: {'sub_questions': [...], 'reasoning': '...'}"
146
+ )
147
+ needs_refinement: bool = Field(
148
+ default=False,
149
+ description="๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€์กฑํ•˜์—ฌ ์ฟผ๋ฆฌ ๊ฐœ์„  ํ•„์š” ์—ฌ๋ถ€"
150
+ )
151
+ refinement_count: int = Field(
152
+ default=0,
153
+ description="๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ๊ฐœ์„  ์‹œ๋„ ํšŸ์ˆ˜ (์ตœ๋Œ€ 1ํšŒ)"
154
+ )
155
+ original_question: Optional[str] = Field(
156
+ default=None,
157
+ description="์ฟผ๋ฆฌ ๊ฐœ์„  ์ „ ์›๋ณธ ์งˆ๋ฌธ (์ตœ์ข… ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹œ ์ฐธ์กฐ)"
158
+ )
159
+
160
+ # Phase 4: Dynamic Parallel Search for Multiple Questions
161
+ is_multi_question: bool = Field(
162
+ default=False,
163
+ description="ํ˜„์žฌ ๋‹ค์ค‘ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ์ค‘์ธ์ง€ ์—ฌ๋ถ€"
164
+ )
165
+ sub_question_index: int = Field(
166
+ default=0,
167
+ description="์„œ๋ธŒ ์งˆ๋ฌธ ์ธ๋ฑ์Šค (0๋ถ€ํ„ฐ ์‹œ์ž‘)"
168
+ )
169
+ sub_question_text: Optional[str] = Field(
170
+ default=None,
171
+ description="ํ˜„์žฌ ์ฒ˜๋ฆฌ ์ค‘์ธ ์„œ๋ธŒ ์งˆ๋ฌธ ํ…์ŠคํŠธ"
172
+ )
173
+ original_multi_question: Optional[str] = Field(
174
+ default=None,
175
+ description="๋‹ค์ค‘ ์งˆ๋ฌธ์˜ ์›๋ณธ ์งˆ๋ฌธ (ํ†ตํ•ฉ ๋‹ต๋ณ€ ์ƒ์„ฑ ์‹œ ์ฐธ์กฐ)"
176
+ )
177
+ multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = Field(
178
+ default_factory=list,
179
+ description="๋‹ค์ค‘ ์งˆ๋ฌธ์˜ ๊ฐ ๋‹ต๋ณ€ ๋ฆฌ์ŠคํŠธ (reducer๋กœ ์ž๋™ ๋ณ‘ํ•ฉ)"
180
+ )
181
+
182
+ class Config:
183
+ 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,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")
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
+ else:
152
+ logger.error("GitHub ๊ฒ€์ƒ‰ HTTP ์—๋Ÿฌ: %s", e, exc_info=True)
153
+ return []
154
+ except Exception as e:
155
+ logger.error("GitHub ๊ฒ€์ƒ‰ ์‹คํŒจ: %s", e, exc_info=True)
156
+ return []
157
+
158
+
159
+ def search_official_docs(query: str, limit: int = 3) -> List[SearchResult]:
160
+ """Tavily API๋ฅผ ์‚ฌ์šฉํ•ด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•œ๋‹ค.
161
+
162
+ Args:
163
+ query: ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ
164
+ limit: ๋ฐ˜ํ™˜ํ•  ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜
165
+
166
+ Returns:
167
+ SearchResult ๋ฆฌ์ŠคํŠธ (์‹คํŒจ ์‹œ ๋นˆ ๋ฆฌ์ŠคํŠธ)
168
+ """
169
+ if not query.strip():
170
+ logger.warning("Official Docs ๊ฒ€์ƒ‰: ๋นˆ ์ฟผ๋ฆฌ")
171
+ return []
172
+
173
+ api_key = os.getenv("TAVILY_API_KEY")
174
+ if not api_key:
175
+ logger.error("TAVILY_API_KEY ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
176
+ return []
177
+
178
+ try:
179
+ client = TavilyClient(api_key=api_key)
180
+
181
+ response = client.search(
182
+ query=query,
183
+ search_depth="basic",
184
+ max_results=limit,
185
+ include_domains=[
186
+ "docs.python.org",
187
+ "docs.oracle.com",
188
+ "spring.io/guides",
189
+ "developer.mozilla.org",
190
+ "reactjs.org/docs",
191
+ ],
192
+ )
193
+
194
+ results = []
195
+ for item in response.get("results", []):
196
+ content = item.get("content", "")
197
+ url = item.get("url", "")
198
+ score = item.get("score", 0.5) # Tavily๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ด€๋ จ๋„ ์ ์ˆ˜
199
+
200
+ results.append(
201
+ SearchResult(
202
+ source="Official Docs",
203
+ content=content,
204
+ url=url,
205
+ relevance_score=score,
206
+ )
207
+ )
208
+
209
+ logger.info("Tavily ๊ฒ€์ƒ‰ ์„ฑ๊ณต: %d๊ฐœ ๊ฒฐ๊ณผ", len(results))
210
+ return results
211
+
212
+ except Exception as e:
213
+ logger.error("Tavily ๊ฒ€์ƒ‰ ์‹คํŒจ: %s", e, exc_info=True)
214
+ return []
215
+
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")
27
+ qdrant_api_key = os.getenv("QDRANT_API_KEY")
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/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
+ - **์ฒซ ์งˆ๋ฌธ**: 10~15์ดˆ ์†Œ์š” (๊ฒ€์ƒ‰ + ๋‹ต๋ณ€ ์ƒ์„ฑ)
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
+ )
CodeWeaver/uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
DYNAMIC_PARALLEL_SEARCH.md ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dynamic Parallel Search for Multiple Independent Questions
2
+
3
+ ## ๊ฐœ์š”
4
+
5
+ CodeWeaver Phase 4๋Š” **๋‹ค์ค‘ ๋…๋ฆฝ ์งˆ๋ฌธ**์„ Send API๋กœ ๋™์  ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•˜์—ฌ, ๊ฐ ์งˆ๋ฌธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ๊ฒ€์ƒ‰ ํŒŒ์ดํ”„๋ผ์ธ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
6
+
7
+ ### ํ•ต์‹ฌ ์ฒ ํ•™
8
+
9
+ > "๊ธฐ์กด ๊ทธ๋ž˜ํ”„๋ฅผ 100% ์žฌ์‚ฌ์šฉํ•˜๋˜, ์งˆ๋ฌธ ๊ฐœ์ˆ˜๋งŒํผ ๋ณต์ œํ•ด์„œ ๋ณ‘๋ ฌ ์‹คํ–‰ํ•œ๋‹ค"
10
+
11
+ - **๊ธฐ์กด ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ๋ฅ **: ~95%
12
+ - **์ƒˆ๋กœ์šด ๋…ธ๋“œ**: 5๊ฐœ ์ถ”๊ฐ€
13
+ - **์ƒˆ๋กœ์šด edge ํ•จ์ˆ˜**: 1๊ฐœ ์ถ”๊ฐ€ (fanout_multi_questions)
14
+ - **์ˆ˜์ •๋œ ๋…ธ๋“œ**: 2๊ฐœ ์ˆ˜์ • (create_plan, generate_answer)
15
+
16
+ ## ์ฃผ์š” ๊ธฐ๋Šฅ
17
+
18
+ ### 1. ์ž๋™ ์งˆ๋ฌธ ์œ ํ˜• ๊ฐ์ง€
19
+
20
+ **create_plan_node**๊ฐ€ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ 3๊ฐ€์ง€ ์ผ€์ด์Šค๋กœ ๋ถ„๋ฅ˜:
21
+
22
+ #### Case 1: single_topic
23
+ - **์ •์˜**: ํ•˜๋‚˜์˜ ์ฃผ์ œ๋ฅผ ๋‹ค๊ฐ๋„๋กœ ๋ฌป๋Š” ๊ฒฝ์šฐ
24
+ - **์˜ˆ์‹œ**: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•"
25
+ - **์„œ๋ธŒ์งˆ๋ฌธ**: ["๊ฐœ๋…", "๊ตฌํ˜„", "์˜ˆ์ œ"] (๋‹ต๋ณ€ ์„น์…˜ ๊ตฌ์กฐ์šฉ)
26
+ - **์‹คํ–‰**: ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ (๊ฒ€์ƒ‰์€ ์›๋ณธ ์งˆ๋ฌธ์œผ๋กœ)
27
+
28
+ #### Case 2: multiple_questions
29
+ - **์ •์˜**: ์„œ๋กœ ๋ฌด๊ด€ํ•œ ๋…๋ฆฝ ์งˆ๋ฌธ (์ตœ๋Œ€ 2๊ฐœ)
30
+ - **์˜ˆ์‹œ**: "JWT๊ฐ€ ๋ญ์•ผ? CORS๋Š”?"
31
+ - **์„œ๋ธŒ์งˆ๋ฌธ**: ["JWT๊ฐ€ ๋ญ์•ผ?", "CORS๋Š”?"] (๊ฐ๊ฐ ๋ณ„๋„ ๊ฒ€์ƒ‰)
32
+ - **์‹คํ–‰**: Send API๋กœ ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰
33
+
34
+ #### Case 3: too_many
35
+ - **์ •์˜**: ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ
36
+ - **์˜ˆ์‹œ**: "JWT? CORS? Docker?"
37
+ - **์‹คํ–‰**: ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ, ๋Œ€ํ™” ๊ณ„์† ๊ฐ€๋Šฅ
38
+ - **ํ•˜๋“œ ๊ฐ€๋“œ**: LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ฌผ์Œํ‘œ ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๋˜๋Š” ์งˆ๋ฌธ ํ›„๋ณด ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ)๋กœ ๊ฒฐ์ •๋ก ์  ์ฐจ๋‹จ
39
+
40
+ ### 2. ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ
41
+
42
+ ๋น„์šฉ ๋ฐ ํ’ˆ์งˆ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด **์ตœ๋Œ€ 2๊ฐœ ์งˆ๋ฌธ**์œผ๋กœ ์ œํ•œ:
43
+
44
+ ```
45
+ ์ž…๋ ฅ: "JWT? CORS? Docker? Redis?"
46
+ ์ฒ˜๋ฆฌ: too_many ์ผ€์ด์Šค โ†’ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
47
+ ์•ˆ๋‚ด: "ํ•˜๋‚˜์˜ ์ฃผ์ œ๋กœ ํ†ตํ•ฉ" ๋˜๋Š” "2๊ฐœ๋งŒ ์„ ํƒ" ๊ถŒ์žฅ
48
+ ```
49
+
50
+ ### 3. Send API ๋™์  ๋ณต์ œ
51
+
52
+ **์ค‘์š”**: LangGraph์—์„œ `List[Send]`๋Š” ๋…ธ๋“œ ๋ฐ˜ํ™˜๊ฐ’์ด ์•„๋‹ˆ๋ผ **conditional edge ํ•จ์ˆ˜ ๋ฐ˜ํ™˜๊ฐ’**์œผ๋กœ๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
53
+
54
+ ```python
55
+ # initiate_dynamic_search_node: state ์ค€๋น„๋งŒ (dict ๋ฐ˜ํ™˜)
56
+ def initiate_dynamic_search_node(state: AgentState) -> dict:
57
+ return {"intermediate_steps": [...]} # Send ๋ฐ˜ํ™˜ ์•ˆ ํ•จ!
58
+
59
+ # fanout_multi_questions: conditional edge ํ•จ์ˆ˜ (List[Send] ๋ฐ˜ํ™˜)
60
+ def fanout_multi_questions(state: AgentState) -> List[Send]:
61
+ sends = []
62
+ for i, question in enumerate(["JWT๊ฐ€ ๋ญ์•ผ?", "CORS๋Š”?"]):
63
+ child_state = state.model_copy(deep=True)
64
+ child_state.user_question = question
65
+ child_state.is_multi_question = True
66
+ # ... ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ • ...
67
+ sends.append(Send("run_single_question_worker", child_state))
68
+ return sends
69
+
70
+ # run_single_question_worker: ๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์‹คํ–‰
71
+ # ๊ฐ Send๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋‚ด๋ถ€ ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰:
72
+ # analyze โ†’ cache โ†’ classify โ†’ search(ร—3) โ†’ collect โ†’ eval โ†’ subgraph โ†’ generate
73
+ # โ†’ multi_answers์— ๊ฒฐ๊ณผ ์ถ”๊ฐ€
74
+ ```
75
+
76
+ ### 4. Reducer ์ž๋™ Fan-in (Reset ๊ธฐ๋Šฅ ํฌํ•จ)
77
+
78
+ ```python
79
+ # State ์ •์˜ (์ปค์Šคํ…€ reducer ์‚ฌ์šฉ)
80
+ multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []
81
+
82
+ # merge_multi_answers reducer:
83
+ # - ๊ธฐ๋ณธ ๋™์ž‘: old + new (๋ณ‘๋ ฌ worker์—์„œ ๋‹ต๋ณ€์„ ๋™์‹œ์— append)
84
+ # - ๋ฆฌ์…‹ ๋™์ž‘: new์˜ ์ฒซ ์›์†Œ๊ฐ€ {"__token__": "__RESET_MULTI_ANS__"}์ด๋ฉด
85
+ # old๋ฅผ ๋ฒ„๋ฆฌ๊ณ  new[1:]๋กœ ๊ต์ฒด (์ด์ „ ํ„ด ๋ˆ„์  ๋ฐฉ์ง€)
86
+
87
+ # run_single_question_worker 1์ด ๋ฆฌํ„ด:
88
+ {"multi_answers": [{"index": 0, "question": "JWT๊ฐ€ ๋ญ์•ผ?", "answer": "..."}]}
89
+
90
+ # run_single_question_worker 2๊ฐ€ ๋ฆฌํ„ด:
91
+ {"multi_answers": [{"index": 1, "question": "CORS๋Š”?", "answer": "..."}]}
92
+
93
+ # LangGraph Reducer๊ฐ€ ์ž๋™ ๋ณ‘ํ•ฉ:
94
+ state.multi_answers = [
95
+ {"index": 0, ...},
96
+ {"index": 1, ...}
97
+ ]
98
+
99
+ # combine_answers_node๊ฐ€ ์ด๋ฅผ ํ†ตํ•ฉ Markdown์œผ๋กœ ๋ณ€ํ™˜
100
+ ```
101
+
102
+ ## ๊ทธ๋ž˜ํ”„ ํ๋ฆ„
103
+
104
+ ```mermaid
105
+ graph TD
106
+ START[START] --> plan[create_plan]
107
+
108
+ plan -->|single_topic| analyze[analyze_question]
109
+ plan -->|multiple_questions 2๊ฐœ| dynamic[initiate_dynamic_search]
110
+ plan -->|too_many 3+| tooMany[handle_too_many_questions]
111
+
112
+ tooMany --> END
113
+
114
+ analyze --> cache[check_cache]
115
+ cache -->|hit| returnCache[return_cached_answer]
116
+ cache -->|miss| classify[classify_intent]
117
+
118
+ returnCache --> END
119
+
120
+ classify --> searchSO[search_stackoverflow]
121
+ classify --> searchGH[search_github]
122
+ classify --> searchDocs[search_official_docs]
123
+
124
+ searchSO --> collect[collect_results]
125
+ searchGH --> collect
126
+ searchDocs --> collect
127
+
128
+ collect --> eval[evaluate_results]
129
+
130
+ eval -->|needs_refinement| refine[refine_search]
131
+ eval -->|sufficient| filterNode[filter_and_score]
132
+
133
+ refine --> classify
134
+
135
+ filterNode --> summarize[summarize_results]
136
+ summarize --> generate[generate_answer]
137
+
138
+ generate -->|is_multi_question| combine[combine_answers]
139
+ generate -->|single_topic| END
140
+
141
+ combine --> END
142
+
143
+ dynamic --> fanout[fanout_multi_questions<br/>conditional edge]
144
+ fanout -.Send Q1.-> worker1[run_single_question_worker<br/>๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„]
145
+ fanout -.Send Q2.-> worker2[run_single_question_worker<br/>๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„]
146
+ worker1 --> combine
147
+ worker2 --> combine
148
+ ```
149
+
150
+ ### ํ๋ฆ„ ์„ค๋ช…
151
+
152
+ #### Single Topic (๊ธฐ์กด ๋™์ž‘ ์œ ์ง€)
153
+ ```
154
+ START โ†’ create_plan (case: single_topic)
155
+ โ†’ analyze โ†’ cache โ†’ classify โ†’ search(ร—3) โ†’ collect โ†’ eval โ†’ subgraph โ†’ generate โ†’ END
156
+ ```
157
+
158
+ #### Multiple Questions (์‹ ๊ทœ)
159
+ ```
160
+ START โ†’ create_plan (case: multiple_questions)
161
+ โ†’ initiate_dynamic_search (state ์ค€๋น„)
162
+ โ†’ fanout_multi_questions (conditional edge)
163
+ โ”œโ”€ Send("run_single_question_worker", Q1) โ†’ [๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ] โ†’ multi_answers[0]
164
+ โ””โ”€ Send("run_single_question_worker", Q2) โ†’ [๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ] โ†’ multi_answers[1]
165
+ โ†’ combine_answers (์ž๋™ fan-in) โ†’ END
166
+ ```
167
+
168
+ #### Too Many (์‹ ๊ทœ)
169
+ ```
170
+ START โ†’ create_plan (case: too_many)
171
+ โ†’ handle_too_many_questions โ†’ END
172
+ (์‚ฌ์šฉ์ž๋Š” ์ฆ‰์‹œ ๋‹ค์‹œ ์งˆ๋ฌธ ๊ฐ€๋Šฅ)
173
+ ```
174
+
175
+ ## ๊ตฌํ˜„ ์ƒ์„ธ
176
+
177
+ ### State ํ™•์žฅ
178
+
179
+ ```python
180
+ # src/agent/state.py
181
+
182
+ class AgentState(BaseModel):
183
+ # ... ๊ธฐ์กด ํ•„๋“œ ...
184
+
185
+ # Phase 4: Dynamic Parallel Search
186
+ is_multi_question: bool = False
187
+ sub_question_index: int = 0
188
+ sub_question_text: Optional[str] = None
189
+ original_multi_question: Optional[str] = None
190
+ multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []
191
+ ```
192
+
193
+ ### ์ƒˆ๋กœ์šด ๋…ธ๋“œ (5๊ฐœ)
194
+
195
+ #### 1. create_plan_node (์ˆ˜์ •)
196
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 206
197
+ - **์—ญํ• **: ์งˆ๋ฌธ ์œ ํ˜• ๋ฐ ๊ฐœ์ˆ˜ ํŒ๋‹จ
198
+ - **๋ณ€๊ฒฝ**:
199
+ - `case` ํ•„๋“œ ์ถ”๊ฐ€ (single_topic/multiple_questions/too_many)
200
+ - **ํ•˜๋“œ ๊ฐ€๋“œ ์ถ”๊ฐ€**: `_hard_guard_too_many` ํ•จ์ˆ˜๋กœ 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ ๊ฒฐ์ •๋ก ์  ์ฐจ๋‹จ
201
+ - ๋ฌผ์Œํ‘œ ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๋˜๋Š” ์งˆ๋ฌธ ํ›„๋ณด ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๊ฐ์ง€
202
+ - LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ `too_many`๋กœ ๊ฐ•์ œ
203
+
204
+ #### 2. handle_too_many_questions_node (์‹ ๊ทœ)
205
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 1068
206
+ - **์—ญํ• **: 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€
207
+ - **ํŠน์ง•**: ๋Œ€ํ™” ์ข…๋ฃŒํ•˜์ง€ ์•Š์Œ (์ฆ‰์‹œ ์žฌ์งˆ๋ฌธ ๊ฐ€๋Šฅ)
208
+
209
+ #### 3. initiate_dynamic_search_node (์‹ ๊ทœ)
210
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 1092
211
+ - **์—ญํ• **: ๋‹ค์ค‘ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ์ง„์ž…์ , state ์ค€๋น„
212
+ - **ํ•ต์‹ฌ**: dict๋งŒ ๋ฐ˜ํ™˜ (Send๋Š” ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Œ)
213
+
214
+ #### 4. fanout_multi_questions (์‹ ๊ทœ - Edge ํ•จ์ˆ˜)
215
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 1110
216
+ - **์—ญํ• **: conditional edge ํ•จ์ˆ˜๋กœ `List[Send]` ๋ฐ˜ํ™˜
217
+ - **ํ•ต์‹ฌ**: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ์„ `run_single_question_worker`๋กœ Send
218
+
219
+ #### 5. run_single_question_worker_node (์‹ ๊ทœ)
220
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 1306
221
+ - **์—ญํ• **: ๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰ํ•˜์—ฌ state ์ถฉ๋Œ ๋ฐฉ์ง€
222
+ - **ํ•ต์‹ฌ**:
223
+ - ๋…๋ฆฝ๋œ ๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„๋ฅผ ๋‚ด๋ถ€์—์„œ ์‹คํ–‰
224
+ - outer graph์˜ scalar state ์ฑ„๋„ ์ถฉ๋Œ ๋ฐฉ์ง€
225
+ - ๊ฒฐ๊ณผ๋ฅผ `multi_answers` reducer์—๋งŒ ์ถ”๊ฐ€
226
+
227
+ #### 6. combine_answers_node (์‹ ๊ทœ)
228
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 1168
229
+ - **์—ญํ• **: multi_answers๋ฅผ ํ†ตํ•ฉ Markdown ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜
230
+ - **ํŠน์ง•**: ์ž๋™ fan-in (๋ชจ๋“  Send ์™„๋ฃŒ ๋Œ€๊ธฐ)
231
+
232
+ ### ์ˆ˜์ •๋œ ๋…ธ๋“œ (1๊ฐœ)
233
+
234
+ #### generate_answer_node (5์ค„ ์ถ”๊ฐ€)
235
+ - **์œ„์น˜**: `src/agent/nodes.py` ๋ผ์ธ 726
236
+ - **์ถ”๊ฐ€ ๋‚ด์šฉ**:
237
+ ```python
238
+ # ๊ธฐ์กด ๋กœ์ง ๋งˆ์ง€๋ง‰์— ์ถ”๊ฐ€
239
+ if state.is_multi_question:
240
+ updates["multi_answers"] = [{
241
+ "index": state.sub_question_index,
242
+ "question": state.sub_question_text,
243
+ "answer": final_answer
244
+ }]
245
+ ```
246
+
247
+ ### ๊ทธ๋ž˜ํ”„ ์žฌ๊ตฌ์„ฑ
248
+
249
+ ```python
250
+ # src/agent/graph.py
251
+
252
+ # 1. START ์ง„์ž…์  ๋ณ€๊ฒฝ
253
+ graph.add_edge(START, "create_plan") # ๊ธฐ์กด: analyze_question
254
+
255
+ # 2. create_plan ํ›„ ๋ถ„๊ธฐ ์ถ”๊ฐ€
256
+ graph.add_conditional_edges(
257
+ "create_plan",
258
+ route_after_plan,
259
+ {
260
+ "analyze_question": "analyze_question",
261
+ "initiate_dynamic_search": "initiate_dynamic_search",
262
+ "handle_too_many_questions": "handle_too_many_questions"
263
+ }
264
+ )
265
+
266
+ # 3. initiate_dynamic_search ํ›„ fan-out
267
+ graph.add_conditional_edges(
268
+ "initiate_dynamic_search",
269
+ fanout_multi_questions, # List[Send] ๋ฐ˜ํ™˜
270
+ )
271
+
272
+ # 4. run_single_question_worker ํ›„ fan-in
273
+ graph.add_edge("run_single_question_worker", "combine_answers")
274
+
275
+ # 5. generate_answer ํ›„ ๋ถ„๊ธฐ ์ถ”๊ฐ€
276
+ graph.add_conditional_edges(
277
+ "generate_answer",
278
+ route_after_generate,
279
+ {
280
+ "combine_answers": "combine_answers",
281
+ END: END
282
+ }
283
+ )
284
+ ```
285
+
286
+ ## ์‚ฌ์šฉ ์˜ˆ์‹œ
287
+
288
+ ### ์˜ˆ์‹œ 1: ๋‹จ์ผ ์ฃผ์ œ (๊ธฐ์กด ๋™์ž‘)
289
+
290
+ ```python
291
+ from CodeWeaver.src.agent.graph import create_agent
292
+ from langchain_core.messages import HumanMessage
293
+
294
+ agent = create_agent()
295
+
296
+ result = await agent.ainvoke({
297
+ "user_question": "React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ",
298
+ "messages": [HumanMessage(content="React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ")]
299
+ })
300
+
301
+ # ๊ฒฐ๊ณผ
302
+ # plan.case: "single_topic"
303
+ # plan.sub_questions: ["hooks๋ž€", "์ฃผ์š” hooks", "์‹ค๋ฌด ํŒจํ„ด"]
304
+ # ํ๋ฆ„: ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ ์‹คํ–‰
305
+ # ์ถœ๋ ฅ: ์ผ๋ฐ˜ ๋‹ต๋ณ€ ํ˜•์‹
306
+ ```
307
+
308
+ ### ์˜ˆ์‹œ 2: ๋‹ค์ค‘ ๋…๋ฆฝ ์งˆ๋ฌธ (์‹ ๊ทœ)
309
+
310
+ ```python
311
+ result = await agent.ainvoke({
312
+ "user_question": "JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?",
313
+ "messages": [HumanMessage(content="JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?")]
314
+ })
315
+
316
+ # ๊ฒฐ๊ณผ
317
+ # plan.case: "multiple_questions"
318
+ # plan.sub_questions: ["JWT๊ฐ€ ๋ญ์•ผ?", "CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?"]
319
+ # ํ๋ฆ„: Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰
320
+ # ์ถœ๋ ฅ:
321
+ ```
322
+
323
+ **์ถœ๋ ฅ ์˜ˆ์‹œ**:
324
+ ```markdown
325
+ # ๋‹ค์ค‘ ์งˆ๋ฌธ ๋‹ต๋ณ€
326
+
327
+ ์›๋ณธ ์งˆ๋ฌธ: JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?
328
+
329
+ ---
330
+
331
+ ## 1. JWT๊ฐ€ ๋ญ์•ผ?
332
+
333
+ JWT(JSON Web Token)๋Š” ์ธ์ฆ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ...
334
+
335
+ [์ƒ์„ธ ๋‹ต๋ณ€...]
336
+
337
+ ---
338
+
339
+ ## 2. CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?
340
+
341
+ CORS(Cross-Origin Resource Sharing) ์—๋Ÿฌ๋Š”...
342
+
343
+ [์ƒ์„ธ ๋‹ต๋ณ€...]
344
+ ```
345
+
346
+ ### ์˜ˆ์‹œ 3: ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ
347
+
348
+ ```python
349
+ result = await agent.ainvoke({
350
+ "user_question": "JWT? CORS? Docker?",
351
+ "messages": [HumanMessage(content="JWT? CORS? Docker?")]
352
+ })
353
+
354
+ # ๊ฒฐ๊ณผ
355
+ # plan.case: "too_many"
356
+ # ์ถœ๋ ฅ:
357
+ ```
358
+
359
+ **์ถœ๋ ฅ ์˜ˆ์‹œ**:
360
+ ```
361
+ ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 2๊ฐœ์˜ ์งˆ๋ฌธ๊นŒ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
362
+
363
+ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์„œ ๋‹ค์‹œ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”:
364
+
365
+ 1. **ํ•˜๋‚˜์˜ ์ฃผ์ œ๋กœ ํ†ตํ•ฉํ•ด์„œ ์งˆ๋ฌธ**
366
+ ์˜ˆ: "JWT ์ธ์ฆ๊ณผ CORS ์„ค์ •์„ ํ•จ๊ป˜ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•"
367
+
368
+ 2. **๊ฐ€์žฅ ์ค‘์š”ํ•œ 2๊ฐœ ์งˆ๋ฌธ๋งŒ ์„ ํƒ**
369
+ ์˜ˆ: "JWT๊ฐ€ ๋ญ์•ผ? ๋‚ด ์ฝ”๋“œ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•ด?"
370
+
371
+ 3. **์งˆ๋ฌธ์„ ๋‚˜๋ˆ ์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์งˆ๋ฌธ**
372
+ ์˜ˆ: ๋จผ์ € "JWT๊ฐ€ ๋ญ์•ผ?" ์งˆ๋ฌธ โ†’ ๋‹ต๋ณ€ ํ™•์ธ โ†’ ๋‹ค์Œ ์งˆ๋ฌธ
373
+
374
+ ์–ด๋–ป๊ฒŒ ๋„์™€๋“œ๋ฆด๊นŒ์š”?
375
+ ```
376
+
377
+ ## ํ…Œ์ŠคํŠธ
378
+
379
+ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— ์žˆ์Šต๋‹ˆ๋‹ค. (์‚ญ์ œ๋จ - ํ•„์š”์‹œ ์žฌ์ƒ์„ฑ)
380
+
381
+ ### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค
382
+
383
+ 1. โœ… **๋‹จ์ผ ์ฃผ์ œ**: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•"
384
+ - ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ ์‹คํ–‰
385
+ - multi_answers ๋น„์–ด์žˆ์Œ
386
+ - ์ผ๋ฐ˜ ๋‹ต๋ณ€ ํ˜•์‹
387
+
388
+ 2. โœ… **๋‹ค์ค‘ ์งˆ๋ฌธ 2๊ฐœ**: "JWT๊ฐ€ ๋ญ์•ผ? CORS๋Š”?"
389
+ - Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰
390
+ - multi_answers์— 2๊ฐœ ํ•ญ๋ชฉ
391
+ - ์„น์…˜ ๊ตฌ๋ถ„๋œ ํ†ตํ•ฉ ๋‹ต๋ณ€
392
+
393
+ 3. โœ… **์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ**: "JWT? CORS? Docker?"
394
+ - handle_too_many_questions๋กœ ๋ถ„๊ธฐ
395
+ - ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
396
+ - ๋Œ€ํ™” ๊ณ„์† ๊ฐ€๋Šฅ
397
+
398
+ 4. โœ… **์—ฃ์ง€ ์ผ€์ด์Šค**: "JWT? CORS? Docker? Redis?"
399
+ - **ํ•˜๋“œ ๊ฐ€๋“œ๋กœ ๋ฌด์กฐ๊ฑด too_many ์ฐจ๋‹จ** (๋ฌผ์Œํ‘œ 4๊ฐœ ๊ฐ์ง€)
400
+ - LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ฐจ๋‹จ ๋ณด์žฅ
401
+
402
+ ## ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ
403
+
404
+ ### ๋ณ‘๋ ฌ ์‹คํ–‰
405
+ - **๋‹จ์ผ ์ฃผ์ œ**: 3๊ฐœ ๊ฒ€์ƒ‰ ๋…ธ๋“œ ๋ณ‘๋ ฌ (๊ธฐ์กด)
406
+ - **๋‹ค์ค‘ ์งˆ๋ฌธ (2๊ฐœ)**: 2ร—3=6๊ฐœ ๊ฒ€์ƒ‰ ๋…ธ๋“œ ๋ณ‘๋ ฌ
407
+ - LangGraph Send API๊ฐ€ ์ž๋™ ๋ณ‘๋ ฌํ™” ๊ด€๋ฆฌ
408
+
409
+ ### ๋น„์šฉ ๊ด€๋ฆฌ
410
+ - ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ: ์ตœ๋Œ€ 2๊ฐœ
411
+ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜: ์†Œ์Šค๋‹น 3-5๊ฐœ
412
+ - ๋‹ค์ค‘ ์งˆ๋ฌธ ์‹œ ์˜๋„ ๋ถ„๋ฅ˜ ์ƒ๋žต (๊ธฐ๋ณธ๊ฐ’ "learning" ์‚ฌ์šฉ)
413
+
414
+ ### ์บ์‹ฑ
415
+ - **๋‹จ์ผ ์ฃผ์ œ**: ์ „์ฒด ๋‹ต๋ณ€ ์บ์‹œ โœ…
416
+ - **๋‹ค์ค‘ ์งˆ๋ฌธ**: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ ๋‹ต๋ณ€ ๊ฐœ๋ณ„ ์บ์‹œ โœ…
417
+ - Q1 ๋‹ต๋ณ€ โ†’ Q1 ์งˆ๋ฌธ์œผ๋กœ ์บ์‹œ
418
+ - Q2 ๋‹ต๋ณ€ โ†’ Q2 ์งˆ๋ฌธ์œผ๋กœ ์บ์‹œ
419
+ - ๋‹ค์Œ๋ฒˆ ๋™์ผ ์งˆ๋ฌธ ์‹œ ๊ฐœ๋ณ„ ์บ์‹œ ํžˆํŠธ ๊ฐ€๋Šฅ
420
+
421
+ ## ๊ธฐ์ˆ ์  ํ•ต์‹ฌ
422
+
423
+ ### 1. Send API ํŒจํ„ด (Conditional Edge ํ•จ์ˆ˜ ์‚ฌ์šฉ)
424
+
425
+ ```python
426
+ # โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ๋…ธ๋“œ์—์„œ Send ๋ฐ˜ํ™˜
427
+ def initiate_dynamic_search_node(state):
428
+ return [Send(...), Send(...)] # ์—๋Ÿฌ ๋ฐœ์ƒ!
429
+
430
+ # โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: conditional edge ํ•จ์ˆ˜์—์„œ Send ๋ฐ˜ํ™˜
431
+ def fanout_multi_questions(state: AgentState) -> List[Send]:
432
+ sends = []
433
+ for i, question in enumerate(sub_questions):
434
+ child_state = state.model_copy(deep=True)
435
+ child_state.user_question = question
436
+ sends.append(Send("run_single_question_worker", child_state))
437
+ return sends
438
+
439
+ # ๊ทธ๋ž˜ํ”„ ์„ค์ •
440
+ graph.add_conditional_edges(
441
+ "initiate_dynamic_search",
442
+ fanout_multi_questions, # List[Send] ๋ฐ˜ํ™˜
443
+ )
444
+
445
+ # LangGraph๊ฐ€ ์ž๋™์œผ๋กœ:
446
+ # 1. ๋‘ Send๋ฅผ ๋ณ‘๋ ฌ ์‹คํ–‰
447
+ # 2. ๊ฐ Send์˜ ๋ชจ๋“  ๋…ธ๋“œ ์‹คํ–‰ ๋Œ€๊ธฐ
448
+ # 3. ๋‹ค์Œ ๊ณตํ†ต ๋…ธ๋“œ๋กœ ์ด๋™ (combine_answers)
449
+ ```
450
+
451
+ ### 2. Reducer ์ž๋™ ๋ณ‘ํ•ฉ (Reset ๊ธฐ๋Šฅ ํฌํ•จ)
452
+
453
+ ```python
454
+ # State ์ •์˜ (์ปค์Šคํ…€ reducer)
455
+ multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []
456
+
457
+ # merge_multi_answers reducer:
458
+ def merge_multi_answers(old: List[Dict], new: List[Dict]) -> List[Dict]:
459
+ if not new:
460
+ return old
461
+ # Reset ํ† ํฐ ์ฒดํฌ
462
+ if new[0].get("__token__") == "__RESET_MULTI_ANS__":
463
+ return new[1:] # ์ด์ „ ํ„ด ๋ˆ„์  ๋ฐฉ์ง€
464
+ return old + new # ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ
465
+
466
+ # create_plan_node์—์„œ ๋งค ์‹คํ–‰ ์‹œ์ž‘ ์‹œ ๋ฆฌ์…‹:
467
+ updates["multi_answers"] = [{"__token__": "__RESET_MULTI_ANS__"}]
468
+
469
+ # ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ:
470
+ # [Q1_answer] + [Q2_answer] = [Q1_answer, Q2_answer]
471
+ ```
472
+
473
+ ### 3. Fan-in ๋ณด์žฅ
474
+
475
+ ```python
476
+ # ๋ชจ๋“  ๊ฒ€์ƒ‰ ๋…ธ๋“œ๊ฐ€ collect_results๋กœ ์—ฐ๊ฒฐ
477
+ graph.add_edge("search_stackoverflow", "collect_results")
478
+ graph.add_edge("search_github", "collect_results")
479
+ graph.add_edge("search_official_docs", "collect_results")
480
+
481
+ # LangGraph๊ฐ€ ์ž๋™์œผ๋กœ:
482
+ # 1. 3๊ฐœ ๊ฒ€์ƒ‰ ๋ชจ๋‘ ์™„๋ฃŒ ๋Œ€๊ธฐ
483
+ # 2. collect_results 1ํšŒ๋งŒ ์‹คํ–‰
484
+ ```
485
+
486
+ ## ์ฝ”๋“œ ๋ณ€๊ฒฝ ์š”์•ฝ
487
+
488
+ ### ํŒŒ์ผ๋ณ„ ๋ณ€๊ฒฝ์‚ฌํ•ญ
489
+
490
+ | ํŒŒ์ผ | ์ถ”๊ฐ€ | ์ˆ˜์ • | ์‚ญ์ œ |
491
+ |------|------|------|------|
492
+ | `state.py` | 5 ํ•„๋“œ, 1 reducer ํ•จ์ˆ˜ | - | - |
493
+ | `nodes.py` | 5 ๋…ธ๋“œ + 1 edge ํ•จ์ˆ˜ (~300์ค„) | 2 ๋…ธ๋“œ (create_plan ํ•˜๋“œ ๊ฐ€๋“œ ์ถ”๊ฐ€, generate_answer 5์ค„) | - |
494
+ | `graph.py` | 3 routing ํ•จ์ˆ˜, ์—ฃ์ง€ ์žฌ๊ตฌ์„ฑ | build_agent_graph | - |
495
+
496
+ **์ด ๋ณ€๊ฒฝ๋Ÿ‰**: ~350์ค„ ์ถ”๊ฐ€, ~100์ค„ ์ˆ˜์ •
497
+
498
+ ### ์žฌ์‚ฌ์šฉ๋ฅ 
499
+
500
+ - **๊ธฐ์กด ๋…ธ๋“œ ์žฌ์‚ฌ์šฉ**: 12/16 (75%)
501
+ - **๊ธฐ์กด ๋กœ์ง ์žฌ์‚ฌ์šฉ**: ~95% (๊ฒ€์ƒ‰, ํ‰๊ฐ€, ํ•„ํ„ฐ๋ง, ์š”์•ฝ ๋“ฑ)
502
+ - **์ƒˆ๋กœ์šด ๊ฐœ๋…**: Send API + Reducer๋งŒ
503
+
504
+ ## LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ ์ค€์ˆ˜
505
+
506
+ ### โœ… Graph API
507
+ - StateGraph ์‚ฌ์šฉ
508
+ - Pydantic BaseModel state
509
+ - START/END ๋ช…์‹œ
510
+
511
+ ### โœ… Workflows + Agents
512
+ - Send API๋กœ ๋™์  ๋ณ‘๋ ฌํ™”
513
+ - Conditional edges๋กœ ๋ผ์šฐํŒ…
514
+ - Fan-out/Fan-in ํŒจํ„ด
515
+
516
+ ### โœ… Thinking in LangGraph
517
+ - ๋…ธ๋“œ๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜ (ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ)
518
+ - State๋Š” ๋ถˆ๋ณ€ ์—…๋ฐ์ดํŠธ
519
+ - Reducer๋กœ ๋ณ‘ํ•ฉ ์ž๋™ํ™”
520
+
521
+ ## ํ•œ๊ณ„ ๋ฐ ํ–ฅํ›„ ๊ฐœ์„ 
522
+
523
+ ### ํ˜„์žฌ ํ•œ๊ณ„
524
+
525
+ 1. **์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ**: ์ตœ๋Œ€ 2๊ฐœ
526
+ - ๋น„์šฉ vs ํ’ˆ์งˆ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„
527
+ - ํ–ฅํ›„ 3-4๊ฐœ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ
528
+
529
+ 2. **์บ์‹ฑ ์ „๋žต**: ํ†ตํ•ฉ ๋‹ต๋ณ€์€ ์บ์‹œ ์•ˆ ๋จ
530
+ - ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ์€ ๊ฐœ๋ณ„ ์บ์‹œ๋จ
531
+ - ๋™์ผํ•œ ๋‹ค์ค‘ ์งˆ๋ฌธ ์žฌ์ž…๋ ฅ ์‹œ ๊ฐœ๋ณ„ ์บ์‹œ ํžˆํŠธ
532
+
533
+ 3. **Refinement ๋ฃจํ”„**: ๋‹ค์ค‘ ์งˆ๋ฌธ์—์„œ๋„ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™
534
+ - ํ•œ ์งˆ๋ฌธ refine ์‹œ ๋‹ค๋ฅธ ์งˆ๋ฌธ์— ์˜ํ–ฅ ์—†์Œ
535
+
536
+ ### ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ
537
+
538
+ 1. **๋” ๋งŽ์€ ์งˆ๋ฌธ ์ง€์›**: 3-4๊ฐœ๊นŒ์ง€ ํ™•์žฅ
539
+ 2. **ํ˜ผํ•ฉ ์งˆ๋ฌธ ๊ฐ์ง€**: "JWT๊ฐ€ ๋ญ์•ผ? ๊ทธ๊ฑธ Spring์— ์ ์šฉํ•˜๋ ค๋ฉด?" (์ˆœ์ฐจ ์˜์กด)
540
+ 3. **์ŠคํŠธ๋ฆฌ๋ฐ ๋‹ต๋ณ€**: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ ์™„๋ฃŒ ์ฆ‰์‹œ ์ŠคํŠธ๋ฆฌ๋ฐ
541
+ 4. **์šฐ์„ ์ˆœ์œ„**: ์ค‘์š”๋„์— ๋”ฐ๋ผ ์งˆ๋ฌธ ์ˆœ์„œ ์กฐ์ •
542
+
543
+ ## ์ฐธ๊ณ  ์ž๋ฃŒ
544
+
545
+ - [LangGraph Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api)
546
+ - [LangGraph Workflows + Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents)
547
+ - [LangGraph Thinking Guide](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph)
548
+ - CodeWeaver Phase 3: Open Deep Research
549
+
550
+ ## ๋ฌธ์˜
551
+
552
+ ๊ตฌํ˜„ ๊ด€๋ จ ์งˆ๋ฌธ์ด๋‚˜ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋Š” ์ด์Šˆ๋กœ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.
553
+
README.md CHANGED
@@ -1,12 +1,39 @@
1
  ---
2
- title: Codeweaver Ai
3
- emoji: ๐Ÿ”ฅ
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.1.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: codeweaver-ai
3
+ emoji: ๐Ÿค–
4
  colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: "4.44.1"
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # CodeWeaver AI (Gradio Space)
14
+
15
+ CodeWeaver๋ฅผ Hugging Face Spaces์—์„œ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Gradio ๋ฐ๋ชจ์ž…๋‹ˆ๋‹ค.
16
+
17
+ ## ์‹คํ–‰ ๋ฐฉ์‹
18
+
19
+ - Space ์—”ํŠธ๋ฆฌ: `app.py` (repo root)
20
+ - ์‹ค์ œ Gradio UI: `CodeWeaver/ui/app.py`
21
+
22
+ ## ํ•„์ˆ˜ Secrets (Settings โ†’ Variables and secrets)
23
+
24
+ - `GOOGLE_API_KEY`
25
+ - `TAVILY_API_KEY`
26
+ - `QDRANT_URL`
27
+ - `QDRANT_API_KEY`
28
+
29
+ ์„ ํƒ:
30
+
31
+ - `GITHUB_TOKEN`
32
+ - `LANGCHAIN_TRACING_V2`, `LANGCHAIN_API_KEY`, `LANGCHAIN_PROJECT`
33
+
34
+ ## ๋ฌธ์„œ
35
+
36
+ - `ARCHITECTURE.md`
37
+ - `DYNAMIC_PARALLEL_SEARCH.md`
38
+
39
+
app.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces entrypoint.
3
+
4
+ This file is intentionally minimal:
5
+ - It imports the existing Gradio Blocks app from `CodeWeaver/ui/app.py`
6
+ - It launches it with HF-friendly defaults.
7
+
8
+ Local dev remains unchanged:
9
+ - You can still run `python CodeWeaver/ui/app.py` as before.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def _bootstrap_import_path() -> None:
20
+ # Make `CodeWeaver/` importable as a top-level path so we can `import ui.app`.
21
+ repo_root = Path(__file__).resolve().parent
22
+ codeweaver_root = repo_root / "CodeWeaver"
23
+ sys.path.insert(0, str(codeweaver_root))
24
+
25
+
26
+ def main() -> None:
27
+ _bootstrap_import_path()
28
+
29
+ # Import AFTER sys.path tweak
30
+ from ui.app import app as demo # type: ignore
31
+
32
+ # HF Spaces commonly provides PORT; fall back to 7860 for local.
33
+ port = int(os.getenv("PORT", "7860"))
34
+ demo.launch(server_name="0.0.0.0", server_port=port, show_api=False)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
39
+
40
+
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces installs dependencies from the repository root.
2
+ # Reuse the project's existing dependency list.
3
+
4
+ -r CodeWeaver/requirements.txt
5
+
6
+