ใ……ใ…Žใ…‡ commited on
Commit
ea80cdc
ยท
0 Parent(s):

Initial commit for Hugging Face Spaces

Browse files
Files changed (7) hide show
  1. .gitignore +34 -0
  2. ARCHITECTURE.md +231 -0
  3. CodeWeaver +1 -0
  4. DYNAMIC_PARALLEL_SEARCH.md +553 -0
  5. README.md +40 -0
  6. app.py +41 -0
  7. requirements.txt +7 -0
.gitignore ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+
11
+ # Environment
12
+ .env
13
+ .venv
14
+ env/
15
+ venv/
16
+ ENV/
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ *.swo
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Logs
29
+ *.log
30
+
31
+ # Lock files (HF Spaces will install from requirements.txt)
32
+ uv.lock
33
+ poetry.lock
34
+
ARCHITECTURE.md ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
231
+
CodeWeaver ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit fc4c811e94059981ae4ef7924c9aed6ccc9cbc44
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 ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
40
+
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
41
+
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
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
+
7
+