lesson-loop
by C-Lake864
README.md
# ๐ Lesson Loop
**๊ฐ๋
์ด๋ฆ ํ๋๋ฅผ ๋ฃ์ผ๋ฉด, ์ถ์ฒ ์กฐ์ฌ๋ถํฐ ๊ฒ์๊น์ง ์ค์ค๋ก ๋๋ ค์ ๋ฐ๋ก ์์
์ ์ธ ์ ์๋ ๋ ์จ ํฉ์ ํ์ผ๋ก ๋ง๋ค์ด ์ฃผ๋ ์์ด์ ํฑ ์ํฌํ๋ก ์๋น์ค.**
ํ์ต ์ฝํ
์ธ ์ ์์๋ ๊ฐ๋
ํ๋๋ง๋ค ๊ฐ์ 6๋จ๊ณ๋ฅผ ์์ผ๋ก ๋ฐ๋ณตํ๋ค โ ์๋ฌธ ์ฐพ๊ธฐ โ ํ์ต๋ชฉํ ์ชผ๊ฐ๊ธฐ โ ๋ฃจ๋ธ๋ฆญ ๋ง๋ค๊ธฐ โ ๋ฌธํญ ์ฐ๊ธฐ โ **๊ทผ๊ฑฐ ๋์กฐ** โ ํ์ผ ์ ๋ฆฌ. ๊ฐ๋
๋น 2~3์๊ฐ์ด ๋ ๋ค. Lesson Loop๋ ์ด ์ ์ฐจ ์ ์ฒด๋ฅผ ๋์ ๋๋ฆฌ๊ณ , ์ฌ๋์ ๋ ์ง์ ์์๋ง ํ์ธํ๋ค.
> ์๋ํํ๋ ๊ฒ์ "๋ฌธํญ ์์ฑ"์ด ์๋๋ผ **"์ถ์ฒ ์กฐ์ฌ โ ์์ฑ โ ๊ทผ๊ฑฐ ๋์กฐ โ ์ฌ๋ ์น์ธ"์ด๋ผ๋ ์ ์ฐจ**๋ค.
> ๊ทธ๋์ ๋ชจ๋ ๋ฌธํญ์ ์ถ์ฒ ์๋ฌธ์ **์ค์ ๋ฌธ์ฅ**์ ๊ทผ๊ฑฐ๋ก ๊ฐ๋๋ค. ๊ทผ๊ฑฐ๋ฅผ ๋ชป ๋๋ฉด ์ฝ๋๊ฐ ์ ์ฅ์ ๊ฑฐ๋ถํ๋ค.
- ๐ ๊ธฐํ ๋ฌธ์: **[PRD.md](PRD.md)**
- ๐ ์คํ ๋ถ์: **[evals/results/ANALYSIS.md](evals/results/ANALYSIS.md)** ยท ์์๋ฃ: [RESULTS.md](evals/results/RESULTS.md)
- ๐ผ **์คํ ๊ฒฐ๊ณผ ์บก์ฒ: [docs/screenshots/](docs/screenshots/)** โ ํ๋ฉด 7์ฅ
- ๐ ๊ฒฐ๊ณผ๋ฌผ ์์: **[docs/sample_lesson_pack.md](docs/sample_lesson_pack.md)** โ ์ค์ ๋ก ๋ง๋ค์ด ๋ธ ๋ ์จ ํฉ
- ๐ง ๋๊ตฌ ์คํค๋ง ์๋ณธ: [docs/tools_schema.json](docs/tools_schema.json)
---
## 1๋ถ ์์ฝ
```
๊ฐ๋
"HTTP ์บ์ฑ" โโ
โผ
โ ๊ณํ ๊ธฐํ์๊ฐ ํ์ ์ฃผ์ ์ ์์ด ๊ฒ์์ด๋ฅผ ๋ฝ๋๋ค
โก ์กฐ์ฌ web_search โ fetch_page ๋ก 1์ฐจ ์ถ์ฒ(MDNยทRFCยทWikipedia)๋ฅผ ํ๋ณด
โธ ์ฌ๋ ํ์ธ ์ถ์ฒ๋ฅผ ๋นผ๊ฑฐ๋ ์ง์ ์ถ๊ฐํ๋ค โ ๊ฐ์ฅ ์ผ ๊ต์ ์ง์
โข ์์ฑ ํ์ต๋ชฉํยท๋ฃจ๋ธ๋ฆญยท๋ฌธํญ. ์ธ์ฉ๋ฌธ์ ์๋ฌธ์์ ๊ทธ๋๋ก ๋ณต์ฌ
โฃ ๊ฒ์ ๋ค๋ฅธ ์์ด์ ํธ๊ฐ ์๋ฌธ๊ณผ ๋์กฐ. ๋ฌธ์ ์์ผ๋ฉด ํด๋น ๋ฌธํญ๋ง ์ฌ์์ฑ
โธ ์ฌ๋ ํ์ธ ๋ฐํ ์น์ธ โ ๋๋๋ฆฌ๊ธฐ ์ด๋ ค์ด ์์
์ง์
โค ๋ฐํ lesson_pack.md + lesson_pack.json
```
ํ๊ฐ ์ธํธ 10๊ฐ ์ค์ธก (gpt-4o-mini): **์๋ฃ์จ 10/10 ยท ๊ทผ๊ฑฐ์จ 100% (190๋ฌธํญ ์ ๋ถ) ยท ์ค์๊ฐ 71~83์ด ยท ํ๊ท $0.021 ยท ํ๊ท 26๋จ๊ณ.**
์์ธํ ์์น์ ๊ฐ์ ์ด๋ ฅ์ [ANALYSIS.md](evals/results/ANALYSIS.md).
---
## ๋น ๋ฅด๊ฒ ์คํํ๊ธฐ
```bash
git clone https://github.com/C-Lake864/lesson-loop.git && cd lesson-loop
python -m venv .venv && .venv/Scripts/activate # macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # .env๋ฅผ ์ด์ด OPENAI_API_KEY๋ฅผ ์ฑ์ด๋ค
uvicorn app.main:app --reload --port 8000
```
๋ธ๋ผ์ฐ์ ์์ <http://localhost:8000> ์ ์ฐ๋ค.
**์น ์์ด ํ ํ ๋๋ ค ๋ณด๊ธฐ** (ํธ๋ ์ด์ค๊ฐ ํฐ๋ฏธ๋์ ๊ทธ๋๋ก ํ๋ฅธ๋ค):
```bash
python -m app.cli "HTTP ์บ์ฑ" --questions 5 --auto
```
### ํ๊ฒฝ๋ณ์
| ๋ณ์ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช
|
| --- | :-: | --- | --- |
| `OPENAI_API_KEY` | โ
| โ | ์๋ฒ์์๋ง ์ฐ์ธ๋ค. ํ๋ก ํธ์๋๋ก ์ ๋ ๋ด๋ ค๊ฐ์ง ์๋๋ค |
| `LESSONLOOP_MODEL` | | `gpt-4o-mini` | ๊ธฐ๋ณธ ๋ชจ๋ธ |
| `LESSONLOOP_REVIEW_MODEL` | | (๋์ผ) | ๊ฒ์์๋ง ๋ค๋ฅธ ๋ชจ๋ธ๋ก ์ฌ๋ฆฌ๊ณ ์ถ์ ๋ |
| `LESSONLOOP_MAX_STEPS` | | `50` | run๋น LLM ํธ์ถ ์ํ |
| `LESSONLOOP_MAX_COST_USD` | | `0.50` | run๋น ๋น์ฉ ์ํ (๋์ผ๋ฉด ์๋ ์ค๋จ) |
| `LESSONLOOP_MAX_SECONDS` | | `420` | run๋น ์๊ฐ ์ํ |
| `LESSONLOOP_MAX_REVISIONS` | | `1` | ๊ฒ์ โ ์ฌ์์ฑ ์ต๋ ํ์ |
| `TAVILY_API_KEY` | | โ | ์์ผ๋ฉด ๊ฒ์ 1์์๋ก ์ฌ์ฉ. ์์ด๋ ๋์ํ๋ค |
| `LESSONLOOP_WEBHOOK_TOKEN` | | โ | ์ค์ ํ๋ฉด `POST /api/webhook/run` ํ์ฑํ |
| `LESSONLOOP_SCHEDULER` | | `0` | `1`์ด๋ฉด ํ๋ฅผ cron์ผ๋ก ์๋ ์คํ |
| `LESSONLOOP_RATE_LIMIT_PER_HOUR` | | `10` | IP๋น ์๊ฐ๋น ์คํ ์ |
์ ์ฒด ๋ชฉ๋ก์ [.env.example](.env.example) ์ฐธ๊ณ . **`.env`๋ ์ ๋ ์ปค๋ฐํ์ง ์๋๋ค** (`.gitignore`์ ์์).
---
## ํ๋ฉด
| ํ๋ฉด | ๊ฒฝ๋ก | ๋ด๋ ๊ฒ |
| --- | --- | --- |
| ์ ์คํ | `/` | ๊ฐ๋
์
๋ ฅ, ์์ฐ ์ฌ๋ผ์ด๋, ์ค์ ์ ํ, ํ, ์ฐ๊ฒฐ๋ ๋๊ตฌ 8์ข
์ ์คํค๋ง |
| ์คํ ์์ธ | `/run/{id}` | **์ค์๊ฐ ํธ๋ ์ด์ค**(SSE) ยท ์ถ์ฒ ยท ๊ฒฐ๊ณผ๋ฌผ ๋ผ์ด๋ธ ๋ทฐ ยท ์น์ธ ํจ๋ ยท ๋ค์ด๋ก๋ |
| ํ๊ฐยท๋น์ฉ | `/evals` | ์ค์ ๋ณ ๋น๊ตํ ยท ๋จ๊ณ๋ณ ๋ณ๋ชฉ ยท ์คํจ ์ฌ๋ก ํ |
| API ๋ฌธ์ | `/api/docs` | FastAPI ์๋ ์์ฑ |
ํธ๋ ์ด์ค์ ๊ฐ ์คํ
์๋ **์ด๋ค ๋๊ตฌ๋ฅผ ์ ๋ถ๋ ๊ณ ๋ฌด์์ ๋ฐ์๋์ง**๊ฐ ์ธ์ยท๊ฒฐ๊ณผ JSON์งธ๋ก ๋จ๊ณ , ์์ ์๊ฐยทํ ํฐยท๋น์ฉ์ด ํจ๊ป ์ฐํ๋ค.
### ์ค์ ํ๋ฉด
| | |
| --- | --- |
| [](docs/screenshots/01-home.png)<br>**โ ์ ์คํ** โ ์์ฐ ์ฌ๋ผ์ด๋, ์ค์ ์ ํ, ์ฐ๊ฒฐ๋ ๋๊ตฌ 8์ข
| [](docs/screenshots/03a-hitl-approval.png)<br>**โก ์ฌ๋ ์น์ธ ๋๊ธฐ** โ ์ถ์ฒ ์ ์ธยท์ถ๊ฐยท์์ฒญ ๋จ๊ธฐ๊ธฐ |
| [](docs/screenshots/03-run-published.png)<br>**โข ๋ฐํ ์๋ฃ** โ ํ์ผ ํฌ๊ธฐ ํ์ธ ํ ๋จ๋ ๋ฐฐ๋, ๊ฒฐ๊ณผ๋ฌผ, ๊ฒ์ ์ง์ , ๋กค๋ฐฑ | [](docs/screenshots/06-evals-dashboard.png)<br>**โฃ ํ๊ฐยท๋น์ฉ** โ ๋จ๊ณ๋ณ ๋ณ๋ชฉ, ์ค์ ๋ณ ๋น๊ต, ์คํจ ์ฌ๋ก |
์ ์ฒด ํธ๋ ์ด์ค(115์คํ
)์ ์ธ์ยท๊ฒฐ๊ณผ JSON๊น์ง ํผ์น ํ๋ฉด, ๋๊ตฌ ์คํค๋ง ์ ๋ฌธ์
**[docs/screenshots/](docs/screenshots/)** ์ ์๋ค. ์ ๋ถ `python docs/capture.py`๋ก ์ฌํ๋๋ค.
---
## ์ฐ๊ฒฐ๋ ๋๊ตฌ 8์ข
| ๋๊ตฌ | ์ข
๋ฅ | ๋ถ์์ฉ | ๊ถํ | ์น์ธ |
| --- | --- | --- | --- | :-: |
| `web_search` | ์ธ๋ถ API | ์์ | ๊ฒ์์ด๋ง ์ ์ก | |
| `fetch_page` | ์ธ๋ถ HTTP | ์์ | **๋๋ฉ์ธ allowlist๋ง**, ์ฌ์ค IP ์ฐจ๋จ, 15์ดยท200KB ์ํ, ์ฃผ์ ๊ด๋ จ์ฑ ๊ฒ์ฌ | |
| `read_sources` | DB ์ฝ๊ธฐ | ์์ | ํ์ฌ run์ ์ถ์ฒ๋ง | |
| `save_draft` | DB ์ฐ๊ธฐ | ์์ | ํ์ฌ run์๋ง insert | |
| `save_questions` | DB ์ฐ๊ธฐ | ์์ | ํ์ฌ run์๋ง insert | |
| `read_draft` | DB ์ฝ๊ธฐ | ์์ | ํ์ฌ run๋ง | |
| `save_review` | DB ์ฐ๊ธฐ | ์์ | ํ์ฌ run์๋ง insert | |
| `export_pack` | **ํ์ผ ์์ฑ** | **๋๋๋ฆฌ๊ธฐ ์ด๋ ค์** | `artifacts/{run_id}/` ๋ฐ์ผ๋ก ๋ชป ๋๊ฐ | **โ
ํ์** |
๊ฐ ๋๊ตฌ์ ์
๋ ฅยท์ถ๋ ฅ ์คํค๋ง๋ ์คํ ํ๋ฉด ์ค๋ฅธ์ชฝ "์ฐ๊ฒฐ๋ ๋๊ตฌ"์์ ํผ์ณ ๋ณผ ์ ์๊ณ , `GET /api/meta`๋ก๋ ๋ฐ์ ์ ์๋ค.
### ์ค๊ณ์์ ์งํจ ๊ฒ
**๋ถ์์ฉ์ ์ ๋ถ ๋๊ตฌ๋ฅผ ํต๊ณผํ๋ค.** ์์ด์ ํธ๋ DB์ ์ง์ ์ฐ์ง ์๊ณ ํ์ผ๋ ์ง์ ๋ง๋ค์ง ์๋๋ค. ๊ทธ๋์ ๋ชจ๋ ๋ถ์์ฉ์ด 100% ๋ก๊ทธ์ ๋จ๋๋ค.
**์น์ธ์ ํ๋กฌํํธ๊ฐ ์๋๋ผ ์ฝ๋๊ฐ ํ์ธํ๋ค.** `export_pack`์ DB์ ์น์ธ ํ ํฐ์ด ์๋์ง ํ์ธํ๊ณ , ์์ผ๋ฉด ๋๊ตฌ ๋ ์ด์ด์์ ๊ฑฐ๋ถํ๋ค. ํ๋กฌํํธ๋ฅผ ์๋ฌด๋ฆฌ ์ค๋ํด๋ ์ฐํํ ์ ์๋ค.
**์คํจ๋ ์์ธ๋ก ํฐ๋จ๋ฆฌ์ง ์๋๋ค.** ๋ชจ๋ ๋๊ตฌ ์ค๋ฅ๋ `{"ok": false, "error": {"code", "message", "hint"}}`๋ก ๋ชจ๋ธ์๊ฒ ๋์๊ฐ๋ค. ์์ด์ ํธ๊ฐ ์ค์ค๋ก ํ๋ณตํ ๊ธฐํ๋ฅผ ์ฃผ๊ธฐ ์ํด์๋ค. ํ๋ณต ์๋๊น์ง ์ ๋ถ ํธ๋ ์ด์ค์ ๋จ๋๋ค.
| ์คํจ ์ ํ | ์ฒ๋ฆฌ |
| --- | --- |
| ํ์์์ยท5xx | ์ฌ์๋ 2ํ (0.5s โ 2s) |
| ๊ฒ์ ๋ฐฑ์๋ ์คํจ | **4๋จ ํด๋ฐฑ**: Tavily โ ๋ฌธ์ ์ฌ์ดํธ ์์ฒด ๊ฒ์(MDNยทWikipedia) โ DuckDuckGo โ ๋ด์ฅ ์๋ ์ฝํผ์ค |
| ํ์ด์ง๋ฅผ ๋ชป ๊ฐ์ ธ์ด | ์๋ ์ฝํผ์ค์ ๊ฐ์ URL์ด ์์ผ๋ฉด ๊ทธ๊ฒ์ผ๋ก ๋์ฒด |
| allowlist ๋ฐ / ์ฃผ์ ๋ฌด๊ด | ์ฌ์๋ ์์ด ๊ตฌ์กฐํ ์ค๋ฅ ๋ฐํ โ ์์ด์ ํธ๊ฐ ๋ค๋ฅธ URL ์ ํ |
| ์คํค๋งยท๊ทผ๊ฑฐ ๊ฒ์ฆ ์คํจ | ๋ฌด์์ด ์ ํ๋ ธ๋์ง + **์๋ฌธ์์ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ตฌ๊ฐ**์ ํจ๊ป ๋ฐํ |
| ๊ฐ์ ๋๊ตฌยท๊ฐ์ ์ธ์ 3ํ | ๋ฃจํ ๊ฐ์ง โ ์ฐจ๋จ |
**๋ฌดํ ๋ฃจํ๋ ์ธ ๊ฒน์ผ๋ก ๋ง๋๋ค.** โ ์์ฐ(๋จ๊ณ 50 / ๋น์ฉ $0.50 / ์๊ฐ 420์ด / ๋๊ตฌ ํธ์ถ 80ํ) โก ๊ฐ์ ํธ์ถ ๋ฐ๋ณต ๊ฐ์ง โข ์ ์ฒด ๊ฐ์ง โ ๋๊ตฌ๋ฅผ ๊ณ์ ๋ถ๋ฅด๋ฉด์ ๋ง๋ฌด๋ฆฌ ๋๊ตฌ๋ฅผ ์ ๋ถ๋ฅด๋ฉด "์ง๊ธ ๊ฐ์ง ์ ๋ณด๋ก ๋ง๋ฌด๋ฆฌํ๋ผ"๋ ์ง์๋ฅผ ์ฃผ์
ํ๋ค.
---
## ์ค๊ฐ์ ๋๊ฒจ๋ ์ด์ด์ง๋ค
์์ด์ ํธ์ ๋ํ ํ์คํ ๋ฆฌ๊ฐ ๋งค ํ์ฐจ DB์ ํต์งธ๋ก ์ ์ฅ๋๋ค. ๊ทธ๋์:
- **์น์ธ ๋๊ธฐ ์ค ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋** ๋์ค์ ๋งํฌ๋ก ๋์์ ์ด์ด์ ์น์ธํ๋ฉด ๊ทธ ์ง์ ๋ถํฐ ์งํ๋๋ค
- **์๋ฒ๋ฅผ ์ฌ์์ํด๋** `running` ์ํ๋ก ์ผ์ด๋ถ์ run์ ์์ ์ ์๋์ผ๋ก ์ด์ด ๋๋ฆฐ๋ค (`recover_interrupted`)
- **์คํจํ run๋** ํ๋ฉด์ "์ด์ด์ ์ฌ๊ฐ" ๋ฒํผ์ผ๋ก ์ ์ฅ๋ ๋จ๊ณ๋ถํฐ ๋ค์ ๋ฏผ๋ค
```bash
# ์ง์ ํ์ธํด ๋ณด๊ธฐ: ์คํ ์ค์ ์๋ฒ๋ฅผ ์ฃฝ์๋ค ์ด๋ฆฐ๋ค
uvicorn app.main:app --port 8000 # Ctrl+C ๋ก ์ค๋จ ํ ๋ค์ ์คํ
# โ ๋ก๊ทธ์ "[lessonloop] ์ค๋จ๋ run N๊ฐ๋ฅผ ์ฌ๊ฐํ๋ค."
```
---
## ์ฌ๋ ๊ฐ์
์ง์
| ์ง์ | ์์น | ํ ์ ์๋ ๊ฒ |
| --- | --- | --- |
| **HITL #1** | ์กฐ์ฌ ์๋ฃ โ ์์ฑ ์์ ์ | ์ถ์ฒ ๊ฐ๋ณ ์ ์ธ ยท URL ์ง์ ์ถ๊ฐ ยท ์์ฑ์์๊ฒ ์์ฒญ ๋จ๊ธฐ๊ธฐ ยท ๊ฑฐ๋ถ ํ ์ฌ์กฐ์ฌ |
| **HITL #2** | ๋ฐํ ์ง์ | ๊ฒฐ๊ณผ๋ฌผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ์ธ ยท ์น์ธ ยท ๋ฐํ ์ทจ์ |
| ์์ | ์คํ ์ค ์ธ์ ๋ | ์ค๋จ(์ฐ์ถ๋ฌผ ๋ณด์กด) ยท ์คํจ ํ ์ฌ๊ฐ ยท ์ด์ ์ด์ ๋ฒ์ ์ผ๋ก ๋กค๋ฐฑ |
`auto_approve`๋ฅผ ์ผ๋ฉด ๋ ์ง์ ์ ์๋ ํต๊ณผํ๋ค. ํ๊ฐ์ฉ์ด๋ฉฐ, ์ผ๋ฉด ํ๋ฉด์ ๊ฒฝ๊ณ ๊ฐ ๋ฌ๋ค.
**"์๋ฃํ๋ค"๋ ๋ง๊ณผ ๊ฒฐ๊ณผ๋ฌผ์ ๋ค๋ฅด๋ค.** ๋ฐํ ์๋ฃ ๋ฐฐ๋๋ ํ์ผ์ด ๋์คํฌ์ ์ค์ ๋ก ์ฐ์๊ณ ํฌ๊ธฐ๊ฐ 0์ด ์๋์ ๋์ฝ์ด ํ์ธํ ๋ค์๋ง ๋จ๊ณ , ํ์ผ๋ช
ยท๋ฐ์ดํธ ์ยท๋ฌธํญ ์ยท๊ทผ๊ฑฐ์จ์ ํจ๊ป ์ฐ๋๋ค.
---
## ์คํ & ํ๊ฐ
```bash
# ์ค์ 4์ข
ร ์๋๋ฆฌ์ค 4์ข
์ ๋๋ฆฌ๊ณ ํ๋ก ๋จ๊ธด๋ค
python -m evals.run --configs baseline,no_review,tools_terse,single_agent \
--only http-caching,css-box-model,hash-table,tx-isolation \
--workers 3
```
- ํ๊ฐ ์ธํธ: [evals/dataset.jsonl](evals/dataset.jsonl) โ ์ค์ ์ปค๋ฆฌํ๋ผ์ ๋ฃ์ ๋ฒํ ๊ฐ๋
10๊ฐ. ๊ฐ ํญ๋ชฉ์ `must_cover`(๋ฐ๋์ ๋ค๋ค์ผ ํ ํ์ ๊ฐ๋
)์ `must_cite_domain`(๊ธฐ๋ํ๋ 1์ฐจ ์ถ์ฒ)์ด ๋ถ์ด ์๋ค.
- ๋น๊ต ์ค์ : [app/configs.py](app/configs.py) โ ํ์ดํ๋ผ์ธ ๋ชจ์ ยท ๋๊ตฌ ์ค๋ช
์์ธ๋ ยท ๊ฒ์ ์ ๋ฌด ยท ๊ณํ ์ ๋ฌด ยท ๋ชจ๋ธ
- **ํด์ยท๊ฐ์ ์ด๋ ฅยท์คํจ ์ฌ๋ก ๋ถ์: [evals/results/ANALYSIS.md](evals/results/ANALYSIS.md)**
- ๋ฐฐ์น ์์๋ฃ: [evals/results/RESULTS.md](evals/results/RESULTS.md)
- ๋ฐฐ์น ๊ฐ ๋น๊ต(ํ์ฌ ์ฑ์ ๊ธฐ์ค์ผ๋ก ์ฌ๊ณ์ฐ): `python -m evals.rescore`
- ์์ฉ ๊ธฐ์ค ์๋ ์ ๊ฒ: `python -m evals.acceptance --base <์ฃผ์> --live`
์งํ๋ ์ ๋ถ ์ฝ๋๊ฐ ๊ณ์ฐํ๋ค. ์์ด์ ํธ๊ฐ "์ ๋๋๋ค"๊ณ ๋งํ ๊ฒ์ ์ธ์ง ์๋๋ค. ํนํ **๊ทผ๊ฑฐ์จ**์ ๋ชจ๋ธ์ ์ฃผ์ฅ์ด ์๋๋ผ ์ธ์ฉ๋ฌธ์ ์๋ฌธ๊ณผ ๋ฌธ์ ๋จ์๋ก ๋์กฐํด ๋ค์ ๊ณ์ฐํ ๊ฐ์ด๋ค.
---
## ๋ฐฐํฌ
> **ํ์ฌ ์ํ**: ๊ณต๊ฐ URL๋ก ๋ฐฐํฌํ์ง๋ ์์๋ค. ๋์ ๋ก์ปฌ์์ ์ค์ ๋ก ๋๋ฆฐ ํ๋ฉด์
> [docs/screenshots/](docs/screenshots/)์ ์บก์ฒ๋ก ๋จ๊ฒผ๊ณ , ์ฐ์ถ๋ฌผ ์์์ ๋๊ตฌ ์คํค๋ง ์๋ณธ๋
> ์ ์ฅ์์ ํจ๊ป ๋์๋ค. ์๋ ์ ์ฐจ๋๋ก ํ๋ฉด ๊ทธ๋๋ก ๋ฐฐํฌ๋๋ค.
ํ ํ๋ก์ธ์ค๊ฐ API์ ์ ์ ํ๋ก ํธ์๋๋ฅผ ํจ๊ป ์๋นํ๋ฏ๋ก ๋ฐฐํฌ ๋จ์๊ฐ ํ๋๋ค.
### ๋ฐฉ๋ฒ A โ Hugging Face Spaces (์นด๋ ๋ฑ๋ก ๋ถํ์, ๊ฐ์ฅ ์ฌ์)
1. <https://huggingface.co/new-space> โ SDK **Docker**, ํ๋์จ์ด **CPU basic (free)** ๋ก Space ์์ฑ
2. Space์ **Settings โ Variables and secrets โ New secret** ์์ `OPENAI_API_KEY` ์ถ๊ฐ
(Secret์ผ๋ก ๋ฃ์ด์ผ ๋ก๊ทธยท์์ค์ ๋
ธ์ถ๋์ง ์๋๋ค)
3. ์ด ์ ์ฅ์๋ฅผ Space์ ์ฌ๋ฆฐ๋ค
```bash
git remote add space https://huggingface.co/spaces/<๊ณ์ >/lesson-loop
git push space main
```
4. Space์ `README.md`๋ YAML ํ๋ก ํธ๋งคํฐ๊ฐ ํ์ํ๋ค. [deploy/huggingface-space-README.md](deploy/huggingface-space-README.md)
๋ฅผ Space ์ ์ฅ์ ๋ฃจํธ์ `README.md`๋ก ๋ณต์ฌํด ์ฐ๋ฉด ๋๋ค (`app_port: 7860`์ด ๋ค์ด ์๋ค)
5. ๋น๋๊ฐ ๋๋๋ฉด `https://<๊ณ์ >-lesson-loop.hf.space/api/meta` ๊ฐ `{"llm_ready": true}` ๋ฅผ ๋ฐํํด์ผ ํ๋ค
### ๋ฐฉ๋ฒ B โ Render
1. Render โ **New โ Web Service** โ ์ด GitHub ์ ์ฅ์ ์ฐ๊ฒฐ (`render.yaml`์ด ์๋ ์ธ์๋๋ค)
2. **Environment** ํญ์์ `OPENAI_API_KEY`๋ฅผ ์ง์ ์
๋ ฅํ๋ค (์ ์ฅ์์๋ ๋ค์ด ์์ง ์๋ค)
3. ๋ฐฐํฌ ํ `/api/meta`๊ฐ `{"llm_ready": true}`๋ฅผ ๋ฐํํ๋ฉด ์ ์
### ๋ฐฉ๋ฒ C โ ์ง์ Docker
```bash
docker build -t lesson-loop .
docker run -p 7860:7860 -e OPENAI_API_KEY=sk-... lesson-loop
```
`PORT` ํ๊ฒฝ๋ณ์๋ฅผ ๋ฐ๋ฅด๋ฏ๋ก FlyยทCloud Run ๋ฑ์๋ ๊ทธ๋๋ก ์ฌ๋ผ๊ฐ๋ค.
> **์๋ ค์ง ํ๊ณ**: SQLite์ ์ฐ์ถ๋ฌผ์ด ์ปจํ
์ด๋ ํ์ผ์์คํ
์ ์์ด ์ฌ๋ฐฐํฌ ์ ์ด๊ธฐํ๋๋ค.
> `LESSONLOOP_DB_PATH`ยท`LESSONLOOP_ARTIFACT_DIR`์ ์๊ตฌ ๋ณผ๋ฅจ์ผ๋ก ์ง์ ํ๋ฉด ์ ์ง๋๋ค.
> HF Spaces์์๋ Persistent Storage(์ ๋ฃ)๋ฅผ ๋ถ์ด๊ฑฐ๋, ๋ฐ๋ชจ ๋ชฉ์ ์ด๋ฉด ๊ทธ๋๋ก ๋์ด๋ ๋ฌด๋ฐฉํ๋ค.
> **๋ฐฐํฌ ์ ๊ถ์ฅ ์ค์ **: ๋จ์ฉ ๋ฐฉ์ง๋ฅผ ์ํด `LESSONLOOP_MAX_COST_USD=0.30`,
> `LESSONLOOP_RATE_LIMIT_PER_HOUR=6` ์ ๋๋ก ๋ฎ์ถฐ ๋๋ ๊ฒ์ด ์ข๋ค.
### ๋ฐฐํฌ ํ ์์ฉ ๊ธฐ์ค
[PRD.md ยง8.2](PRD.md)์ A1~A7์ ์คํฌ๋ฆฝํธ๋ก ๋ค์ ํ์ธํ๋ค.
```bash
python -m evals.acceptance --base https://<๋ฐฐํฌ์ฃผ์> --live --write
```
ํ์ธ ๊ฒฐ๊ณผ๋ [evals/results/RESULTS.md](evals/results/RESULTS.md) ํ๋จ์ ๊ธฐ๋ก๋๋ค.
---
## ํ์ฅ
### MCP ์๋ฒ โ ์ฐ์ถ๋ฌผ์ ๋ค๋ฅธ ์์ด์ ํธ์ ๋๊ตฌ๋ก
์ด ์๋น์ค๋ ๋๊ตฌ๋ฅผ ์ฐ๋ ์ชฝ์ด์, ์๊ธฐ ์ฐ์ถ๋ฌผ์ ๋๊ตฌ๋ก ๋ด๋๋ ์ชฝ์ด๊ธฐ๋ ํ๋ค.
```bash
python mcp_server.py
```
`.mcp.json`์ ๋ฑ๋กํ๋ฉด Claude Code ๊ฐ์ ํด๋ผ์ด์ธํธ๊ฐ ๋ง๋ค์ด์ง ๋ ์จ ํฉ์ ์ง์ ์ธ ์ ์๋ค.
```json
{ "mcpServers": { "lesson-loop": {
"command": "python", "args": ["mcp_server.py"], "cwd": "<์ด ์ ์ฅ์ ๊ฒฝ๋ก>" } } }
```
| MCP ๋๊ตฌ | ํ๋ ์ผ |
| --- | --- |
| `list_packs` | ๋ง๋ค์ด์ง ๋ ์จ ํฉ ๋ชฉ๋ก |
| `get_pack` | ํฉ ํ๋ ์ ์ฒด (json ๋๋ md) |
| `search_questions` | ๋ชจ๋ ํฉ์ ๊ฐ๋ก์ง๋ฌ ๋ฌธํญ ๊ฒ์ โ ๋ฌธ์ ์ํ์ฒ๋ผ |
| `get_rubric` | ํ์ต๋ชฉํยท๋ฃจ๋ธ๋ฆญ๋ง |
| `enqueue_concept` | ์ ๊ฐ๋
์ ์ ์ ํ์ ๋ฑ๋ก |
**๋ฐํยท์ญ์ ๋ ๋
ธ์ถํ์ง ์๋๋ค.** ๋๋๋ฆฌ๊ธฐ ์ด๋ ค์ด ์ผ์ ์ฌ๋ ์น์ธ ์ง์ ์ด ์๋ ์น UI์๋ง ๋จ๊ฒจ ๋๋ค.
### ์๋ ํธ๋ฆฌ๊ฑฐ
```bash
# ์นํ
(LESSONLOOP_WEBHOOK_TOKEN ์ค์ ์)
curl -X POST https://<๋ฐฐํฌ์ฃผ์>/api/webhook/run \
-H "X-Webhook-Token: $TOKEN" -H "content-type: application/json" \
-d '{"concept":"CORS","n_questions":5,"auto_approve":true}'
# ์ค์ผ์ค๋ฌ โ ํ์ ์์ธ ๊ฐ๋
์ cron์ ๋ง์ถฐ ์๋ ์คํ
LESSONLOOP_SCHEDULER=1 LESSONLOOP_SCHEDULER_CRON="0 3 * * *" uvicorn app.main:app
```
### ๊ทธ ๋ฐ์
- **๋ฉํฐ ์์ด์ ํธ** โ ๊ธฐํ์ / ์กฐ์ฌ์ / ์์ฑ์ / ๊ฒ์์ 4์ญํ . `single_agent` ์ค์ ์ผ๋ก ๋จ์ผ ์์ด์ ํธ์ ๋น๊ตํ ์ ์๋ค
- **๋กค๋ฐฑ** โ ์ด์์ append-only๋ก ๋ฒ์ ์ด ์์ธ๋ค. ์คํ ํ๋ฉด์์ ์ด์ ๋ฒ์ ์ผ๋ก ๋์ด๋ฆด ์ ์๋ค
- **๋น์ฉ ๋์๋ณด๋** โ `/evals`์์ ๋จ๊ณ๋ณ ๋์ ์๊ฐยท๋น์ฉ์ผ๋ก ๋ณ๋ชฉ์ ๋ณธ๋ค
---
## ๊ตฌ์กฐ
```
app/
main.py FastAPI โ API ยท SSE ยท ์ ์ ์๋น
orchestrator.py ๋จ๊ณ ์ํ ๋จธ์ ยท ์ฌ๋ ๊ฐ์
์ง์ ยท ์คํจ ๋ถ๋ฅ
agent.py ์์ด์ ํธ ๋ฃจํ (๊ณํ โ ๋๊ตฌ โ ๊ด์ฐฐ โ ๋ค์ ํ๋)
tools/
base.py ๊ถํ ยท ์น์ธ ยท ์ฌ์๋ ยท ๋ฃจํ ๊ฐ์ง๊ฐ ๋ชจ์ด๋ ๋๊ตฌ ์ง์
์
search.py web_search (4๋จ ํด๋ฐฑ)
fetch.py fetch_page (allowlist ยท SSRF ๋ฐฉ์ด ยท ์ฃผ์ ๊ด๋ จ์ฑ)
draft.py save_draft ยท save_questions (๊ฒ์ฆ ๊ฒ์ดํธ)
store.py read_sources (์ธ์ฉ๋ฌธ ์ถ์ถ ํฌํจ) ยท save_review
grounding.py ์ธ์ฉ๋ฌธ โ ์๋ฌธ ๋์กฐ โ ํ๊ฐ ๋ฐฉ์ง์ ํต์ฌ
export.py export_pack (์น์ธ ํ์ ยท ๋์ฝ์ด ํ์ธ)
db.py SQLite โ run ยท trace ยท ๋ฉ์์ง ํ์คํ ๋ฆฌ ยท ์น์ธ ยท ์ํฐํฉํธ
llm.py OpenAI ํธ์ถ + ํ ํฐยท๋น์ฉ ๊ณ๋
prompts.py ์ญํ ๋ณ ์์คํ
ํ๋กฌํํธ
configs.py ์คํ ์ค์
static/ ํ๋ก ํธ์๋ (๋น๋ ๋๊ตฌ ์์)
evals/ ํ๊ฐ ์ธํธ ยท ๋ฌ๋ ยท ๊ฒฐ๊ณผ
mcp_server.py MCP ์๋ฒ
data/seed_sources.json ๊ฒ์์ด ์ ๋ถ ๋งํ์ ๋์ ์ตํ ํด๋ฐฑ ์ฝํผ์ค
```
## ์ธ๋ถ ์๋น์ค ์ฌ์ฉ์ ๋ํด
- ์ถ์ฒ๋ ๊ณต๊ฐ ๊ธฐ์ ๋ฌธ์ ์ฌ์ดํธ๋ก ํ์ ํ๋ค ([app/config.py](app/config.py)์ `FETCH_ALLOWLIST`).
- ๋ณธ๋ฌธ์ **์ธ์ฉ ๋ชฉ์ ์ ๋ฐ์ท**๋ง ์ ์ฅํ๊ณ , ๊ฒฐ๊ณผ๋ฌผ์๋ ์๋ฌธ URL์ ํญ์ ํ๊ธฐํ๋ค.
- MDNยทWikipedia์ ๊ณต๊ฐ ๊ฒ์ ์๋ํฌ์ธํธ๋ฅผ ์ด๋ค. Wikipedia์ User-Agent ์ ์ฑ
์ ๋ง์ถฐ ์ฐ๋ฝ์ฒ๊ฐ ๋ด๊ธด UA๋ฅผ ๋ณด๋ธ๋ค.
- ๊ฐ์ ธ์จ ๋ณธ๋ฌธ์ ํญ์ `<untrusted_document>`๋ก ๊ฐ์ธ ๋ชจ๋ธ์๊ฒ "๋ฐ์ดํฐ์ด์ง ์ง์๊ฐ ์๋"์ ๋ช
์ํ๋ค. ๋ฌธ์์ ํ๋กฌํํธ ์ธ์ ์
์ด ์ฌ๊ฒจ ์์ด๋ ๋๊ตฌ ๊ถํ์ ์ฝ๋๊ฐ ๋ง๋๋ค.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues