ㅅㅎㅇ 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
+