Spaces:
Runtime error
Runtime error
ใ
ใ
ใ
commited on
Commit ยท
515f392
1
Parent(s): 4f3be99
Add CodeWeaver Gradio app
Browse files- ARCHITECTURE.md +230 -0
- CodeWeaver/.env.example +9 -0
- CodeWeaver/.gitignore +23 -0
- CodeWeaver/.python-version +1 -0
- CodeWeaver/README.md +118 -0
- CodeWeaver/main.py +6 -0
- CodeWeaver/pyproject.toml +27 -0
- CodeWeaver/requirements.txt +24 -0
- CodeWeaver/src/__init__.py +0 -0
- CodeWeaver/src/agent/__init__.py +51 -0
- CodeWeaver/src/agent/graph.py +422 -0
- CodeWeaver/src/agent/nodes.py +1387 -0
- CodeWeaver/src/agent/state.py +183 -0
- CodeWeaver/src/tools/__init__.py +12 -0
- CodeWeaver/src/tools/search_tools.py +215 -0
- CodeWeaver/src/utils/__init__.py +7 -0
- CodeWeaver/src/utils/tracing.py +91 -0
- CodeWeaver/src/vector_db/__init__.py +6 -0
- CodeWeaver/src/vector_db/local_embeddings.py +34 -0
- CodeWeaver/src/vector_db/qdrant_client.py +225 -0
- CodeWeaver/ui/app.py +272 -0
- CodeWeaver/uv.lock +0 -0
- DYNAMIC_PARALLEL_SEARCH.md +553 -0
- README.md +32 -5
- app.py +40 -0
- requirements.txt +6 -0
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:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|