Kentlo's picture
Sync from GitHub 37f011465a0aa2abdfdd94d8bd5f31cfe932c669
28677d8 verified
Raw
History Blame Contribute Delete
66.7 kB
[
{
"id": "docs-lab-1",
"title": "[๋‹จ๊ณ„ 1] Ollama โ€” ๋กœ์ปฌ LLM ์„ค์น˜",
"task_description": "**์ƒํ™ฉ**\nOpenAI API๋กœ ๊ฐœ๋ฐœํ•˜๋˜ ์ค‘ ์ฒญ๊ตฌ์„œ๊ฐ€ ๋‚˜์™”์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋งŒ ์ˆ˜์‹ญ ๋ฒˆ ํ–ˆ๋Š”๋ฐ ์š”๊ธˆ์ด ์ฒญ๊ตฌ๋์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ์—์„œ ๋ฌด๋ฃŒ๋กœ ๋Œ๋ฆด ์ˆ˜ ์—†์„๊นŒ์š”?\n\n**๋ฌธ์ œ**\nAPI ํ‚ค ์—†์ด LLM์„ ์“ฐ๋ ค๋ฉด ๋ชจ๋ธ์„ ์ง์ ‘ ๋กœ์ปฌ์— ๋‚ด๋ ค๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nOllama๋กœ `llama3.2` ๋ชจ๋ธ์„ ๋กœ์ปฌ์— ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”. (๋Œ€ํ™” ์—†์ด ๋‹ค์šด๋กœ๋“œ๋งŒ)\n\n> `run`์ด ์•„๋‹Œ, ๋‹ค์šด๋กœ๋“œ๋งŒ ํ•˜๋Š” ๋ช…๋ น์–ด์ž…๋‹ˆ๋‹ค.",
"expected_command": "ollama pull llama3.2",
"grading_conditions": ["ollama pull", "llama3.2"],
"hint": "`ollama pull <๋ชจ๋ธ๋ช…>` ํ˜•์‹์ž…๋‹ˆ๋‹ค.",
"explanation": "`ollama pull`์€ ๋ชจ๋ธ ํŒŒ์ผ์„ ๋กœ์ปฌ์— ๋‚ด๋ ค๋ฐ›์Šต๋‹ˆ๋‹ค. ์ดํ›„ `ollama list`๋กœ ๋ชฉ๋ก ํ™•์ธ, `ollama run llama3.2`๋กœ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. API ํ‚ค๋„, ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ๋„ ์—†์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "API ํ‚ค ์—†์ด ollama pull ํ•œ ๋ฒˆ์œผ๋กœ ๋กœ์ปฌ LLM ์‹คํ–‰ ํ™˜๊ฒฝ์ด ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-2",
"title": "[๋‹จ๊ณ„ 2] Ollama REST API โ€” Python์—์„œ ํ˜ธ์ถœํ•˜๊ธฐ",
"task_description": "**์ƒํ™ฉ**\n`ollama run llama3.2`๋กœ ํ„ฐ๋ฏธ๋„ ๋Œ€ํ™”๋Š” ๋˜๋Š”๋ฐ, ๋‚ด Python ์ฝ”๋“œ์—์„œ LLM์„ ํ˜ธ์ถœํ•˜๋ ค๋‹ˆ ๋ฐฉ๋ฒ•์„ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nCLI ๋Œ€ํ™” ๋ชจ๋“œ๋Š” ์ž๋™ํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ์—์„œ ํ˜ธ์ถœํ•˜๋ ค๋ฉด HTTP API๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\ncurl๋กœ Ollama REST API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•ด ๋™์ž‘์„ ํ™•์ธํ•˜์„ธ์š”.\n- ์—”๋“œํฌ์ธํŠธ: `http://localhost:11434/api/generate`\n- ๋ชจ๋ธ: `llama3.2`, ํ”„๋กฌํ”„ํŠธ: `'2+2๋Š”?'`, ์ŠคํŠธ๋ฆฌ๋ฐ ์—†์ด",
"expected_command": "curl http://localhost:11434/api/generate -d '{\"model\":\"llama3.2\",\"prompt\":\"2+2๋Š”?\",\"stream\":false}'",
"grading_conditions": ["curl", "localhost:11434/api/generate", "stream", "false"],
"hint": "`-d` ํ”Œ๋ž˜๊ทธ๋กœ JSON ๋ฐ”๋””๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. `\"stream\": false`๋ฅผ ๋„ฃ์–ด์•ผ ํ•œ ๋ฒˆ์— ์‘๋‹ต์ด ์˜ต๋‹ˆ๋‹ค.",
"explanation": "Ollama๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ค„ ๋‹จ์œ„ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค. `\"stream\": false`๋ฅผ ๋„ฃ์œผ๋ฉด ์™„์„ฑ๋œ ์‘๋‹ต์„ ํ•œ ๋ฒˆ์— ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด Python์—์„œ ํŒŒ์‹ฑํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "Ollama REST API๋Š” OpenAI ํ˜ธํ™˜ ํ˜•์‹์ด๋ผ ์–ด๋–ค ์–ธ์–ดยทํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋„ ๋™์ผํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-3",
"title": "[๋‹จ๊ณ„ 3] Ollama ์ฑ„ํŒ… API โ€” ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ตฌ์กฐ",
"task_description": "**์ƒํ™ฉ**\n`/api/generate`๋กœ '์ด์ „์— ๋‚ด๊ฐ€ ๋ญ๋ผ๊ณ  ํ–ˆ์–ด?'๋ฅผ ๋ฌผ์–ด๋ดค๋”๋‹ˆ ๋ชจ๋ฅธ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋งค ์š”์ฒญ์ด ์™„์ „ํžˆ ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n`/api/generate`๋Š” ๋‹จ์ผ ํ”„๋กฌํ”„ํŠธ๋งŒ ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๋ถ™์ด๋ ค๋ฉด ๋‹ค๋ฅธ ์—”๋“œํฌ์ธํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`/api/chat`์€ `messages` ๋ฐฐ์—ด๋กœ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. `llama3.2`์—๊ฒŒ `user` ๋ฉ”์‹œ์ง€ `'ํŒŒ์ด์ฌ์ด ๋ญ์•ผ?'`๋ฅผ ๋ณด๋‚ด์„ธ์š”. (stream: false)",
"expected_command": "curl http://localhost:11434/api/chat -d '{\"model\":\"llama3.2\",\"messages\":[{\"role\":\"user\",\"content\":\"ํŒŒ์ด์ฌ์ด ๋ญ์•ผ?\"}],\"stream\":false}'",
"grading_conditions": ["curl", "localhost:11434/api/chat", "messages", "role", "user"],
"hint": "`/api/chat`์€ `messages` ๋ฐฐ์—ด๋กœ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`messages` ๋ฐฐ์—ด์— `{\"role\": \"user\", \"content\": \"...\"}` ํ˜•์‹์œผ๋กœ ์ด์ „ ๋Œ€ํ™”๋ฅผ ์Œ“์œผ๋ฉด ๋ฉ€ํ‹ฐํ„ด ๋Œ€ํ™”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. LangChain์˜ ์ฑ„ํŒ… ๋ชจ๋ธ๋„ ๋‚ด๋ถ€์ ์œผ๋กœ ์ด ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "messages ๋ฐฐ์—ด์— {role, content} ์Œ์„ ์Œ“๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ชจ๋“  LLM ๋ฉ€ํ‹ฐํ„ด ๋Œ€ํ™”์˜ ๊ธฐ๋ณธ ํ˜•์‹์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-4",
"title": "[๋‹จ๊ณ„ 4] .env โ€” API ํ‚ค๋ฅผ ์ฝ”๋“œ์—์„œ ๋ถ„๋ฆฌํ•˜๊ธฐ",
"task_description": "**์ƒํ™ฉ**\n```python\napi_key = 'sk-abc123...'\n```\n์ด๋ ‡๊ฒŒ ์ฝ”๋“œ์— ์ง์ ‘ ์ผ๋‹ค๊ฐ€ GitHub์— pushํ–ˆ๋”๋‹ˆ API ํ‚ค๊ฐ€ ๊ณต๊ฐœ๋์Šต๋‹ˆ๋‹ค. GitHub์ด ์ž๋™์œผ๋กœ ํƒ์ง€ํ•ด์„œ ํ‚ค๋ฅผ ์ฆ‰์‹œ ๋ฌดํšจํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nAPI ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. `.gitignore`์— ์ถ”๊ฐ€ํ•œ ๋ณ„๋„ ํŒŒ์ผ์— ๋ณด๊ด€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`.env` ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๊ณ  `OPENAI_API_KEY`๋ฅผ ์ฝ๋Š” ์ฝ”๋“œ 2์ค„์„ ์ž…๋ ฅํ•˜์„ธ์š”.\n\n```python\nfrom dotenv import load_dotenv\nimport os\n# ์—ฌ๊ธฐ์— ์ž…๋ ฅํ•˜์„ธ์š”\n```",
"expected_command": "load_dotenv()\napi_key = os.getenv('OPENAI_API_KEY')",
"grading_conditions": ["load_dotenv()", "os.getenv", "OPENAI_API_KEY"],
"hint": "`load_dotenv()`๋ฅผ ๋ฐ˜๋“œ์‹œ ๋จผ์ € ํ˜ธ์ถœํ•ด์•ผ `os.getenv()`๊ฐ€ ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"explanation": "`load_dotenv()` ํ˜ธ์ถœ ์ „์— `os.getenv()`๋ฅผ ์“ฐ๋ฉด ํ•ญ์ƒ `None`์ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. `.env` ํŒŒ์ผ์€ `.gitignore`์— ์ถ”๊ฐ€ํ•˜๊ณ , `.env.example`์—๋Š” ํ‚ค ์ด๋ฆ„๋งŒ ์ ์–ด ํŒ€์— ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "load_dotenv() โ†’ os.getenv() ์ˆœ์„œ๊ฐ€ ๋ชจ๋“  Python ํ”„๋กœ์ ํŠธ์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ํ‘œ์ค€์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-5",
"title": "[๋‹จ๊ณ„ 5] ChatPromptTemplate โ€” ํ”„๋กฌํ”„ํŠธ ๊ตฌ์กฐํ™”",
"task_description": "**์ƒํ™ฉ**\n```python\nresponse = llm.invoke(f'๋ฌธ์„œ: {context}\\n์งˆ๋ฌธ: {question}')\n```\nf-string์œผ๋กœ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋”๋‹ˆ ๋ณ€์ˆ˜ ์ˆœ์„œ๋ฅผ ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฝ๊ณ , ์‹œ์Šคํ…œ ์—ญํ• (์–ด์‹œ์Šคํ„ดํŠธ ํŽ˜๋ฅด์†Œ๋‚˜)์„ ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๋‹จ์ˆœ ๋ฌธ์ž์—ด ๋ฐฉ์‹์€ ์—ญํ•  ๊ตฌ๋ถ„์ด ์—†๊ณ  LangChain ํŒŒ์ดํ”„๋ผ์ธ์— ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n์•„๋ž˜ ์กฐ๊ฑด์œผ๋กœ `ChatPromptTemplate`์„ ์ž‘์„ฑํ•˜์„ธ์š”.\n- ์‹œ์Šคํ…œ: `\"๋‹น์‹ ์€ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ AI ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. ์ปจํ…์ŠคํŠธ: {context}\"`\n- ์‚ฌ์šฉ์ž: `\"{question}\"`\n\n๊ฒฐ๊ณผ๋ฅผ `prompt` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "prompt = ChatPromptTemplate.from_messages([\n (\"system\", \"๋‹น์‹ ์€ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ AI ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. ์ปจํ…์ŠคํŠธ: {context}\"),\n (\"human\", \"{question}\")\n])",
"grading_conditions": ["ChatPromptTemplate.from_messages", "system", "context", "human", "question"],
"hint": "`ChatPromptTemplate.from_messages([(\"system\", \"...\"), (\"human\", \"...\")])` ํ˜•์‹์ž…๋‹ˆ๋‹ค.",
"explanation": "ChatPromptTemplate์€ ์—ญํ• ๋ณ„ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๊ณ  ๋ณ€์ˆ˜ ์ž๋ฆฌ(`{context}`, `{question}`)๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. `chain.invoke({\"context\": ..., \"question\": ...})`๋กœ ์‹คํ–‰ ์‹œ ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "ChatPromptTemplate์€ ์—ญํ• (system/human)๊ณผ ๋ณ€์ˆ˜({})๋ฅผ ๋ถ„๋ฆฌํ•ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-6",
"title": "[๋‹จ๊ณ„ 6] LCEL โ€” ์ฒด์ธ์„ ํ•œ ์ค„๋กœ",
"task_description": "**์ƒํ™ฉ**\n```python\nformatted = prompt.format(context=ctx, question=q)\nllm_result = llm.invoke(formatted)\nfinal = output_parser.parse(llm_result)\n```\n3๋‹จ๊ณ„๋ฅผ ๋งค๋ฒˆ ์ง์ ‘ ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค. ์ฒด์ธ์ด ๊ธธ์–ด์งˆ์ˆ˜๋ก ๋” ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๊ฐ ๋‹จ๊ณ„๋ฅผ ์ˆ˜๋™์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊ธธ๊ณ  ๋‹จ๊ณ„ ์ถ”๊ฐ€/๋ณ€๊ฒฝ์ด ๋ฒˆ๊ฑฐ๋กญ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`prompt`, `llm`, `output_parser`๋ฅผ LCEL `|` ์—ฐ์‚ฐ์ž๋กœ ์—ฐ๊ฒฐํ•ด `chain` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "chain = prompt | llm | output_parser",
"grading_conditions": ["chain", "prompt", "|", "llm", "|", "output_parser"],
"hint": "์œ ๋‹‰์Šค ํŒŒ์ดํ”„์ฒ˜๋Ÿผ `|`๋กœ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`chain = prompt | llm | output_parser` ํ•œ ์ค„์ด ์œ„์˜ 3์ค„์„ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค. `chain.invoke({\"context\": ..., \"question\": ...})`๋กœ ์‹คํ–‰ํ•˜๋ฉด ์ˆœ์„œ๋Œ€๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋‹จ๊ณ„ ์ถ”๊ฐ€๋„ `| ์ƒˆ๋‹จ๊ณ„`๋ฅผ ๋ถ™์ด๋ฉด ๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "| ํŒŒ์ดํ”„ ์—ฐ์‚ฐ์ž๋กœ Runnable์„ ์—ฐ๊ฒฐํ•˜๋Š” LCEL์ด LangChain ๋ชจ๋“  ์ฒด์ธ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-7",
"title": "[๋‹จ๊ณ„ 7] TextSplitter โ€” ํ† ํฐ ์ดˆ๊ณผ ํ•ด๊ฒฐ",
"task_description": "**์ƒํ™ฉ**\n100ํŽ˜์ด์ง€ PDF ์ „์ฒด๋ฅผ LLM ํ”„๋กฌํ”„ํŠธ์— ๋„ฃ์—ˆ๋”๋‹ˆ:\n```\nError: This model's maximum context length is 128000 tokens. \nYour message resulted in 210000 tokens.\n```\n๋ฌธ์„œ๊ฐ€ ๊ธธ์ˆ˜๋ก LLM์— ํ†ต์งธ๋กœ ๋„ฃ๋Š” ๊ฑด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nLLM์€ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ์ˆ˜์— ํ•œ๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๋ฅผ ์ž˜๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`RecursiveCharacterTextSplitter`๋กœ ์ฒญํฌ ํฌ๊ธฐ 500, ์˜ค๋ฒ„๋žฉ 50์œผ๋กœ `docs`๋ฅผ ๋ถ„ํ• ํ•ด `chunks`์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)\nchunks = splitter.split_documents(docs)",
"grading_conditions": ["RecursiveCharacterTextSplitter", "chunk_size=500", "chunk_overlap=50", "split_documents"],
"hint": "์ƒ์„ฑ ํ›„ `.split_documents(docs)`๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "chunk_size=500์€ ์ฒญํฌ๋‹น ์ตœ๋Œ€ 500์ž, chunk_overlap=50์€ ์ธ์ ‘ ์ฒญํฌ๊ฐ€ 50์ž๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฒ„๋žฉ์ด ์—†์œผ๋ฉด ์ฒญํฌ ๊ฒฝ๊ณ„์—์„œ ๋ฌธ์žฅ์ด ์ž˜๋ ค ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋–จ์–ด์ง‘๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "chunk_overlap์ด ์—†์œผ๋ฉด ์ฒญํฌ ๊ฒฝ๊ณ„์—์„œ ๋ฌธ๋งฅ์ด ์ž˜๋ ค ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋–จ์–ด์ง‘๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ chunk_size์˜ 10% ์ •๋„๋ฅผ ์”๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-8",
"title": "[๋‹จ๊ณ„ 8] FAISS VectorStore โ€” ์˜๋ฏธ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค ์ƒ์„ฑ",
"task_description": "**์ƒํ™ฉ**\n๋ฌธ์„œ๋ฅผ ์ฒญํฌ๋กœ ์ž˜๋ž๋Š”๋ฐ, ์งˆ๋ฌธ์ด ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค 500๊ฐœ ์ฒญํฌ๋ฅผ ์ „๋ถ€ LLM์— ๋„ฃ์œผ๋ฉด ์ฒ˜์Œ๊ณผ ๋˜‘๊ฐ™์ด ํ† ํฐ์ด ์ดˆ๊ณผ๋ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n\"๊ด€๋ จ ์žˆ๋Š” ์ฒญํฌ๋งŒ\" ์ฐพ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์€ ๋™์˜์–ด๋‚˜ ์˜๋ฏธ์  ์œ ์‚ฌ์„ฑ์„ ๋ชป ์žก์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`chunks`์™€ `embeddings` ๋ชจ๋ธ๋กœ FAISS VectorStore๋ฅผ ๋งŒ๋“ค๊ณ , `./faiss_index`์— ์ €์žฅํ•˜์„ธ์š”. (๋‹ค์Œ ์‹คํ–‰ ์‹œ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด)",
"expected_command": "db = FAISS.from_documents(chunks, embeddings)\ndb.save_local(\"./faiss_index\")",
"grading_conditions": ["FAISS.from_documents", "chunks", "embeddings", "save_local"],
"hint": "`FAISS.from_documents(๋ฌธ์„œ, ์ž„๋ฒ ๋”ฉ๋ชจ๋ธ)`๋กœ ์ƒ์„ฑ, `.save_local(๊ฒฝ๋กœ)`๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "FAISS๋Š” ๊ฐ ์ฒญํฌ๋ฅผ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•ด ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์งˆ๋ฌธ๋„ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•ด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด(์˜๋ฏธ์ ์œผ๋กœ ์œ ์‚ฌํ•œ) ์ฒญํฌ๋ฅผ O(1)์— ๊ฐ€๊น๊ฒŒ ์ฐพ์Šต๋‹ˆ๋‹ค. `save_local`๋กœ ์ €์žฅํ•˜๋ฉด ๋‹ค์Œ ์‹คํ–‰ ์‹œ ์ž„๋ฒ ๋”ฉ ์žฌ๊ณ„์‚ฐ ์—†์ด ๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "FAISS.from_documents()๊ฐ€ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ๊ณผ ์ธ๋ฑ์Šค ๊ตฌ์ถ•์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๊ณ , save_local()๋กœ ์žฌ์‹œ์ž‘ ์‹œ ์žฌ๊ณ„์‚ฐ์„ ํ”ผํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-9",
"title": "[๋‹จ๊ณ„ 9] RAG ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ โ€” ์—ฐ๊ฒฐ",
"task_description": "**์ƒํ™ฉ**\n```python\ndocs = retriever.invoke(question) # 1. ๊ฒ€์ƒ‰\ncontext = '\\n'.join(d.page_content for d in docs) # 2. ํ•ฉ์น˜๊ธฐ\nprompt_text = prompt.format(...) # 3. ํ”„๋กฌํ”„ํŠธ\nresponse = llm.invoke(prompt_text) # 4. LLM ํ˜ธ์ถœ\nfinal = parser.parse(response) # 5. ํŒŒ์‹ฑ\n```\n5์ค„ ์ฝ”๋“œ๋ฅผ ๋งค ์งˆ๋ฌธ๋งˆ๋‹ค ๋ฐ˜๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nretriever โ†’ prompt โ†’ llm โ†’ parser๋ฅผ ์ˆ˜๋™์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์žฅํ™ฉํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nLCEL๋กœ RAG ์ฒด์ธ์„ ํ•œ ์ค„๋กœ ๋งŒ๋“œ์„ธ์š”.\n- `retriever`, `prompt`, `llm`, `StrOutputParser` ๋ณ€์ˆ˜๋Š” ์ด๋ฏธ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.",
"expected_command": "chain = {\"context\": retriever, \"question\": RunnablePassthrough()} | prompt | llm | StrOutputParser()",
"grading_conditions": ["retriever", "RunnablePassthrough", "|", "prompt", "|", "llm", "StrOutputParser"],
"hint": "`{\"context\": retriever, \"question\": RunnablePassthrough()}`๋กœ retriever์™€ ์งˆ๋ฌธ์„ ๋™์‹œ์— prompt์— ๋„˜๊น๋‹ˆ๋‹ค.",
"explanation": "๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋ฌถ์œผ๋ฉด `RunnableParallel`์ฒ˜๋Ÿผ ๋™์ž‘ํ•ด retriever ๊ฒ€์ƒ‰๊ณผ ์งˆ๋ฌธ ์ „๋‹ฌ์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. `RunnablePassthrough()`๋Š” ์ž…๋ ฅ(์งˆ๋ฌธ)์„ ๊ทธ๋Œ€๋กœ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "hard",
"takeaway": "{context: retriever, question: RunnablePassthrough()} ํŒจํ„ด์ด RAG ์ฒด์ธ์—์„œ ๊ฒ€์ƒ‰๊ณผ ์งˆ๋ฌธ์„ ๋™์‹œ์— ํ”„๋กฌํ”„ํŠธ์— ์ฃผ์ž…ํ•˜๋Š” ํ•ต์‹ฌ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-10",
"title": "[๋‹จ๊ณ„ 10] session_state โ€” ๋Œ€ํ™”๊ฐ€ ์‚ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ",
"task_description": "**์ƒํ™ฉ**\nStreamlit ์ฑ—๋ด‡์„ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ์ด์ „ ๋Œ€ํ™”๊ฐ€ ์ „๋ถ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nStreamlit์€ ๋ฒ„ํŠผ ํด๋ฆญ ๋“ฑ ๋ชจ๋“  ์ƒํ˜ธ์ž‘์šฉ๋งˆ๋‹ค Python ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. `messages = []`์ฒ˜๋Ÿผ ์„ ์–ธํ•˜๋ฉด ๋งค๋ฒˆ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`messages` ํ‚ค๊ฐ€ ์—†์„ ๋•Œ๋งŒ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ํŒจํ„ด์„ ์ž‘์„ฑํ•˜์„ธ์š”.\n\n> ํ•ต์‹ฌ: `st.session_state`์— ์ €์žฅ๋œ ๊ฐ’์€ rerunํ•ด๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.",
"expected_command": "if \"messages\" not in st.session_state:\n st.session_state.messages = []",
"grading_conditions": ["\"messages\" not in st.session_state", "st.session_state.messages", "[]"],
"hint": "`if \"ํ‚ค\" not in st.session_state:` ๊ฐ€๋“œ๋ฅผ ์“ฐ๋ฉด ์ฒซ ์‹คํ–‰์—๋งŒ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.",
"explanation": "`if ... not in st.session_state` ํŒจํ„ด์ด ์—†์œผ๋ฉด ๋งค rerun๋งˆ๋‹ค `[]`๋กœ ๋ฆฌ์…‹๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ด๋ฏธ ๊ฐ’์ด ์žˆ์„ ๋•Œ๋Š” ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "`if key not in st.session_state:` ๊ฐ€๋“œ ํŒจํ„ด ํ•˜๋‚˜๊ฐ€ Streamlit ์ƒํƒœ ๋ณด์กด์˜ ์ „๋ถ€์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-11",
"title": "[๋‹จ๊ณ„ 11] cache_resource โ€” ์งˆ๋ฌธ๋งˆ๋‹ค LLM์ด ์ƒˆ๋กœ ๋กœ๋“œ๋˜๋Š” ๋ฌธ์ œ",
"task_description": "**์ƒํ™ฉ**\n์ฑ—๋ด‡์— ์งˆ๋ฌธํ•  ๋•Œ๋งˆ๋‹ค 3~4์ดˆ์”ฉ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ๋กœ๊ทธ๋ฅผ ๋ณด๋‹ˆ LLM ๊ฐ์ฒด๊ฐ€ ๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.\n\n```python\ndef load_llm(): # ์ด ํ•จ์ˆ˜๊ฐ€ ๋งค rerun๋งˆ๋‹ค ์‹คํ–‰๋จ\n return OllamaLLM(model=\"llama3.2\")\n```\n\n**๋ฌธ์ œ**\nStreamlit์€ rerun๋งˆ๋‹ค ๋ชจ๋“  ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. LLM ๋กœ๋“œ๋Š” ๋น„์šฉ์ด ํฌ๋ฏ€๋กœ ํ•œ ๋ฒˆ๋งŒ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`load_llm()` ํ•จ์ˆ˜์— `st.cache_resource` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋ถ™์—ฌ ์•ฑ ์ˆ˜๋ช… ๋™์•ˆ ํ•œ ๋ฒˆ๋งŒ ๋กœ๋“œ๋˜๊ฒŒ ํ•˜์„ธ์š”.",
"expected_command": "@st.cache_resource\ndef load_llm():\n return OllamaLLM(model=\"llama3.2\")",
"grading_conditions": ["@st.cache_resource", "def load_llm", "OllamaLLM"],
"hint": "ํ•จ์ˆ˜ ์ •์˜ ๋ฐ”๋กœ ์œ„์— `@st.cache_resource`๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.",
"explanation": "`@st.cache_resource`๋Š” ๋ฐ˜ํ™˜ ๊ฐ์ฒด๋ฅผ ์•ฑ ์ „์ฒด(๋ชจ๋“  ์„ธ์…˜)์—์„œ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ์—๋งŒ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ดํ›„์—๋Š” ์บ์‹œ๋œ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "@st.cache_resource๋Š” LLMยทDB์ฒ˜๋Ÿผ ๊ณต์œ  ๊ฐ€๋Šฅํ•œ ๋ฌด๊ฑฐ์šด ๊ฐ์ฒด์—, @st.cache_data๋Š” ๊ณ„์‚ฐ ๊ฒฐ๊ณผ(DataFrame ๋“ฑ)์— ์”๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-12",
"title": "[๋‹จ๊ณ„ 12] Streamlit secrets โ€” ๋ฐฐํฌ ํ™˜๊ฒฝ์˜ API ํ‚ค ๊ด€๋ฆฌ",
"task_description": "**์ƒํ™ฉ**\n๋กœ์ปฌ์—์„œ `.env`๋กœ ์ž˜ ์ž‘๋™ํ•˜๋˜ ์ฑ—๋ด‡์„ Streamlit Cloud์— ๋ฐฐํฌํ–ˆ๋”๋‹ˆ:\n```\nKeyError: 'OPENAI_API_KEY'\n```\nStreamlit Cloud๋Š” ๋กœ์ปฌ์˜ `.env` ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ๋Š” `.env` ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ API ํ‚ค๋ฅผ ์ฃผ์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nStreamlit Cloud ๋Œ€์‹œ๋ณด๋“œ์˜ Secrets์— ํ‚ค๋ฅผ ๋“ฑ๋กํ•œ ๋’ค, ์ฝ”๋“œ์—์„œ ์ฝ๋Š” ํ•œ ์ค„์„ ์ž‘์„ฑํ•˜์„ธ์š”.",
"expected_command": "api_key = st.secrets[\"OPENAI_API_KEY\"]",
"grading_conditions": ["st.secrets", "OPENAI_API_KEY"],
"hint": "`st.secrets[\"ํ‚ค์ด๋ฆ„\"]`์œผ๋กœ ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`st.secrets`๋Š” ๋กœ์ปฌ์—์„œ๋Š” `.streamlit/secrets.toml`, ๋ฐฐํฌ ์‹œ์—๋Š” ๋Œ€์‹œ๋ณด๋“œ Secrets์—์„œ ๊ฐ’์„ ์ฝ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š์•„๋„ ๋‘ ํ™˜๊ฒฝ์—์„œ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "st.secrets๋Š” ๋กœ์ปฌ(.streamlit/secrets.toml)๊ณผ ๋ฐฐํฌ(Streamlit Cloud) ํ™˜๊ฒฝ์„ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-13",
"title": "[๋‹จ๊ณ„ 13] ๋Œ€ํ™” ๋ฉ”๋ชจ๋ฆฌ โ€” ์ด์ „ ๋Œ€ํ™”๋ฅผ ๊ธฐ์–ตํ•˜๊ฒŒ",
"task_description": "**์ƒํ™ฉ**\n```\n์‚ฌ์šฉ์ž: ํŒŒ์ด์ฌ์ด ๋ญ์•ผ?\n์ฑ—๋ด‡: ํŒŒ์ด์ฌ์€ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์ž…๋‹ˆ๋‹ค...\n์‚ฌ์šฉ์ž: ๊ทธ๊ฑฐ ์–ด๋””์„œ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด?\n์ฑ—๋ด‡: ๋ฌด์—‡์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”? (??)\n```\n\"๊ทธ๊ฑฐ\"๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ฑ—๋ด‡์ด ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ๋งค ์งˆ๋ฌธ์ด ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nRAG ์ฒด์ธ์€ ์ด์ „ ๋Œ€ํ™” ๋‚ด์šฉ์„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฉ€ํ‹ฐํ„ด ๋Œ€ํ™”๋ฅผ ์œ„ํ•ด ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ฃผ์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n๊ธฐ์กด `chain`์„ `RunnableWithMessageHistory`๋กœ ๊ฐ์‹ธ์„ธ์š”.\n- `input_messages_key`: `\"question\"`\n- `history_messages_key`: `\"chat_history\"`\n- ๊ฒฐ๊ณผ๋ฅผ `chain_with_history`์— ์ €์žฅ",
"expected_command": "chain_with_history = RunnableWithMessageHistory(\n chain,\n get_session_history,\n input_messages_key=\"question\",\n history_messages_key=\"chat_history\"\n)",
"grading_conditions": ["RunnableWithMessageHistory", "chain", "get_session_history", "input_messages_key", "history_messages_key"],
"hint": "`RunnableWithMessageHistory(์ฒด์ธ, ํžˆ์Šคํ† ๋ฆฌํ•จ์ˆ˜, input_messages_key=..., history_messages_key=...)`",
"explanation": "`RunnableWithMessageHistory`๋Š” ๋งค invoke๋งˆ๋‹ค session_id์— ํ•ด๋‹นํ•˜๋Š” ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ๋ถˆ๋Ÿฌ์™€ ํ”„๋กฌํ”„ํŠธ์— ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค. invoke ์‹œ `config={\"configurable\": {\"session_id\": \"user-1\"}}`์„ ๋„˜๊ฒจ์•ผ ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "RunnableWithMessageHistory๋กœ ๊ธฐ์กด ์ฒด์ธ์„ ๊ฐ์‹ธ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ€ํ‹ฐํ„ด ๋Œ€ํ™”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. session_id ๊ด€๋ฆฌ๊ฐ€ ์ „๋ถ€์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-14",
"title": "[๋‹จ๊ณ„ 14] HF Spaces โ€” ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋„ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋ฐฐํฌ",
"task_description": "**์ƒํ™ฉ**\n์™„์„ฑ๋œ ์ฑ—๋ด‡์„ ํŒ€์›์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋ ค๋‹ˆ \"๋กœ์ปฌ์—์„œ๋งŒ ๋Œ์•„๊ฐ€๊ณ  ๋ฐ–์—์„  ์ ‘๊ทผ์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.\"\n\n**๋ฌธ์ œ**\n์„œ๋ฒ„๋ฅผ ์ง์ ‘ ๊ตฌ๋งคํ•˜๊ฑฐ๋‚˜ AWS/GCP๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ ๋„ ์™ธ๋ถ€์— ๊ณต๊ฐœํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nHugging Face Spaces๋กœ ๋ฌด๋ฃŒ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. `README.md` ์ƒ๋‹จ์— SDK๋ฅผ ์„ ์–ธํ•˜์„ธ์š”.\n- SDK: `streamlit`, ์•ฑ ํŒŒ์ผ: `app.py`, Python: `3.11`",
"expected_command": "---\nsdk: streamlit\napp_file: app.py\npython_version: \"3.11\"\n---",
"grading_conditions": ["sdk: streamlit", "app_file: app.py"],
"hint": "README.md ๋งจ ์œ„ `---` YAML ๋ธ”๋ก ์•ˆ์— ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "HF Spaces๋Š” README ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ `sdk` ํ•„๋“œ๋กœ ๋Ÿฐํƒ€์ž„์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. `sdk: streamlit`์ด๋ฉด ์ž๋™์œผ๋กœ Streamlit ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋ฌด๋ฃŒ ํ”Œ๋žœ์œผ๋กœ๋„ ์™ธ๋ถ€ URL์ด ์ฆ‰์‹œ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "README YAML ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ 3์ค„(sdk, app_file, python_version)๋งŒ์œผ๋กœ HF Spaces ๋ฐฐํฌ๊ฐ€ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-15",
"title": "[๋‹จ๊ณ„ 15] HF Docker Space โ€” SDK ์ œ์•ฝ ์—†์ด ๋ฐฐํฌ",
"task_description": "**์ƒํ™ฉ**\nHF Spaces Streamlit SDK๋กœ ๋ฐฐํฌํ–ˆ๋Š”๋ฐ, FastAPI ์„œ๋ฒ„๋ฅผ ํ•จ๊ป˜ ๋„์šฐ๊ณ  ์‹ถ์–ด ํฌํŠธ๋ฅผ ์ปค์Šคํ…€ํ•˜๋ ค๋‹ˆ ์„ค์ •์ด ๋ง‰ํ˜€์žˆ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nStreamlit/Gradio SDK๋Š” ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์ด ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค. ์ง์ ‘ ์„œ๋ฒ„๋ฅผ ์ œ์–ดํ•˜๋ ค๋ฉด Dockerfile์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nDocker Space์—์„œ HF๊ฐ€ ์ธ์‹ํ•˜๋Š” ํฌํŠธ๋กœ ์•ฑ์„ ๋…ธ์ถœํ•˜๋Š” Dockerfile ์ง€์‹œ์–ด๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.\n\n> HF Docker Space ๊ธฐ๋ณธ ํฌํŠธ: 7860",
"expected_command": "EXPOSE 7860\nENV PORT=7860",
"grading_conditions": ["EXPOSE 7860"],
"hint": "Streamlit ๊ธฐ๋ณธ ํฌํŠธ๋Š” 8501์ด์ง€๋งŒ HF๋Š” 7860์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "Docker Space์—์„œ๋Š” `EXPOSE 7860`์œผ๋กœ ํฌํŠธ๋ฅผ ์„ ์–ธํ•˜๊ณ , `streamlit run app.py --server.port 7860`์œผ๋กœ Streamlit๋„ 7860์— ๋ฐ”์ธ๋”ฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "HF Docker Space๋Š” ํ•ญ์ƒ ํฌํŠธ 7860์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. EXPOSE 7860๊ณผ ์•ฑ์˜ ํฌํŠธ ์„ค์ •์„ ์ผ์น˜์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-16",
"title": "[๋‹จ๊ณ„ 16] FastAPI โ€” ์ฑ—๋ด‡ ๋กœ์ง์„ API๋กœ ๋ถ„๋ฆฌ",
"task_description": "**์ƒํ™ฉ**\nํ”„๋ก ํŠธ์—”๋“œ ํŒ€์—์„œ \"์ฑ—๋ด‡ API ์—”๋“œํฌ์ธํŠธ๋งŒ ์ฃผ์„ธ์š”. React์—์„œ ๋ถ™์ด๊ฒ ์Šต๋‹ˆ๋‹ค.\"๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ง€๊ธˆ์€ Streamlit์— ๋กœ์ง์ด ๋ฌถ์—ฌ ์žˆ์–ด ๋ถ„๋ฆฌ๊ฐ€ ์•ˆ ๋ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nStreamlit ์•ฑ์€ UI์™€ ๋กœ์ง์ด ๊ฒฐํ•ฉ๋˜์–ด ์žˆ์–ด ๋‹ค๋ฅธ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n์•„๋ž˜ `???`๋ฅผ ์ฑ„์›Œ POST `/ask` ์—”๋“œํฌ์ธํŠธ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.\n\n```python\nclass Query(BaseModel):\n question: str\n\n@app.post('/ask')\nasync def ask(q: Query):\n answer = ???\n return {\"answer\": answer}\n```",
"expected_command": "answer = chain.invoke(q.question)",
"grading_conditions": ["chain.invoke", "q.question"],
"hint": "Pydantic ๋ชจ๋ธ `q`์˜ `.question` ์†์„ฑ์œผ๋กœ ์งˆ๋ฌธ์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "FastAPI๋Š” Pydantic ๋ชจ๋ธ ํƒ€์ž…์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ง€์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ JSON ๋ฐ”๋””๋ฅผ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. `q.question`์œผ๋กœ ์งˆ๋ฌธ ๋ฌธ์ž์—ด์„ ๊บผ๋‚ด chain์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "FastAPI + Pydantic ์กฐํ•ฉ์ด LLM ์„œ๋น„์Šค๋ฅผ API๋กœ ๋…ธ์ถœํ•˜๋Š” ๊ฐ€์žฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. Pydantic์ด JSON ๊ฒ€์ฆ์„ ์ž๋™ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-17",
"title": "[๋‹จ๊ณ„ 17] FastAPI ๊ฐœ๋ฐœ ์„œ๋ฒ„ โ€” ์ฝ”๋“œ ์ˆ˜์ • ์ฆ‰์‹œ ๋ฐ˜์˜",
"task_description": "**์ƒํ™ฉ**\n```python\nuvicorn main:app\n```\n์—”๋“œํฌ์ธํŠธ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„๋ฅผ `Ctrl+C`๋กœ ์ข…๋ฃŒํ•˜๊ณ  ๋‹ค์‹œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์ค‘์— ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กญ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๊ธฐ๋ณธ uvicorn์€ ํŒŒ์ผ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`main.py`์˜ `app` ์ธ์Šคํ„ด์Šค๋ฅผ ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘๋˜๋Š” ๋ชจ๋“œ๋กœ ์‹คํ–‰ํ•˜์„ธ์š”.",
"expected_command": "uvicorn main:app --reload",
"grading_conditions": ["uvicorn", "main:app", "--reload"],
"hint": "`--reload` ํ”Œ๋ž˜๊ทธ ํ•˜๋‚˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`--reload`๋Š” ์†Œ์Šค ํŒŒ์ผ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ด ์„œ๋ฒ„๋ฅผ ์ž๋™ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์ค‘์—๋งŒ ์‚ฌ์šฉํ•˜๋ฉฐ, ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ œ๊ฑฐํ•˜๊ณ  `gunicorn` ๊ฐ™์€ ํ”„๋กœ๋•์…˜ ์„œ๋ฒ„๋ฅผ ์”๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "--reload๋Š” ๊ฐœ๋ฐœ์šฉ, ํ”„๋กœ๋•์…˜์—์„œ๋Š” gunicorn์„ ์”๋‹ˆ๋‹ค. ์ด ๊ตฌ๋ถ„์„ ๊ธฐ์–ตํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-18",
"title": "[๋‹จ๊ณ„ 18] LangSmith โ€” ์–ด๋А ๋‹จ๊ณ„์—์„œ ํ‹€๋ฆฐ ๋‹ต์ด ๋‚˜์˜ค๋Š”๊ฐ€",
"task_description": "**์ƒํ™ฉ**\n์ฑ—๋ด‡์ด ์—‰๋šฑํ•œ ๋‹ต์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋””๋ฒ„๊น…์„ ์œ„ํ•ด `print()`๋ฅผ ์ถ”๊ฐ€ํ–ˆ์ง€๋งŒ:\n\n```python\nprint('์ฒด์ธ ์‹คํ–‰ ์ค‘...')\nresult = chain.invoke({'question': q})\nprint('๊ฒฐ๊ณผ:', result)\n```\n\n์ตœ์ข… ์ž…์ถœ๋ ฅ๋งŒ ๋ณด์ผ ๋ฟ, **retriever๊ฐ€ ์ž˜๋ชป๋œ ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์™”๋Š”์ง€**, **ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ž˜๋ชป ๋ Œ๋”๋ง๋๋Š”์ง€**, **LLM์ด ํ‹€๋ฆฐ ๋‹ต์„ ํ–ˆ๋Š”์ง€** ๊ตฌ๋ถ„์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n`print()`๋Š” ๊ฐ ๋‹จ๊ณ„์˜ ๋‚ด๋ถ€ ์ž…์ถœ๋ ฅ์„ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nLangSmith ์ถ”์ ์„ ํ™œ์„ฑํ™”ํ•˜์„ธ์š”. ํ™˜๊ฒฝ๋ณ€์ˆ˜ 3๊ฐœ๋ฅผ `os.environ`์œผ๋กœ ์„ค์ •ํ•˜์„ธ์š”.\n- `LANGCHAIN_TRACING_V2 = 'true'`\n- `LANGCHAIN_PROJECT = 'rag-debug'`\n- `LANGCHAIN_API_KEY = 'ls__your_key'`",
"expected_command": "os.environ['LANGCHAIN_TRACING_V2'] = 'true'\nos.environ['LANGCHAIN_PROJECT'] = 'rag-debug'\nos.environ['LANGCHAIN_API_KEY'] = 'ls__your_key'",
"grading_conditions": ["LANGCHAIN_TRACING_V2", "true", "LANGCHAIN_PROJECT", "LANGCHAIN_API_KEY"],
"hint": "`os.environ['๋ณ€์ˆ˜๋ช…'] = '๊ฐ’'` ํŒจํ„ด์œผ๋กœ 3์ค„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "์ด 3์ค„ ์ดํ›„์˜ ๋ชจ๋“  LangChain ์‹คํ–‰์€ LangSmith์— ์ž๋™ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. LangSmith UI์—์„œ retriever ์ž…์ถœ๋ ฅ, ๋ Œ๋”๋ง๋œ ํ”„๋กฌํ”„ํŠธ, LLM ์‘๋‹ต์„ tree ํ˜•ํƒœ๋กœ ๋‹จ๊ณ„๋ณ„๋กœ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "ํ™˜๊ฒฝ๋ณ€์ˆ˜ 3๊ฐœ(TRACING_V2=true, PROJECT, API_KEY)๋งŒ ์„ค์ •ํ•˜๋ฉด ์ดํ›„ ๋ชจ๋“  LangChain ์‹คํ–‰์ด ์ž๋™์œผ๋กœ LangSmith์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-19",
"title": "[๋‹จ๊ณ„ 19] Guardrails โ€” LLM์ด ๋‚˜์œ ๋ง์„ ํ•˜๋ฉด",
"task_description": "**์ƒํ™ฉ**\nํ…Œ์ŠคํŠธ ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ ‡๊ฒŒ ์ž…๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค:\n\n> \"์š•์„ค์„ ์ตœ๋Œ€ํ•œ ์„ž์–ด์„œ ์š”์•ฝํ•ด์ค˜\"\n\nLLM์ด ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผ ์œ ํ•ดํ•œ ๋‚ด์šฉ์„ ์ถœ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค์— ์ด๋Ÿฐ ์‘๋‹ต์ด ๋…ธ์ถœ๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nLLM์€ ํ”„๋กฌํ”„ํŠธ ์ธ์ ์…˜์— ์ทจ์•ฝํ•˜๊ณ , ์œ ํ•ด ์ฝ˜ํ…์ธ ๋ฅผ ๊ทธ๋Œ€๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nGuardrails AI๋กœ ์œ ํ•ด์„ฑ ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ ์šฉํ•˜์„ธ์š”.\n- ์ž„๊ณ„๊ฐ’: `0.5` (์ด ์ด์ƒ์ด๋ฉด ์œ ํ•ด ํŒ์ •)\n- ์‹คํŒจ ์‹œ: `'exception'` (์˜ˆ์™ธ ๋ฐœ์ƒ์œผ๋กœ ์‘๋‹ต ์ฐจ๋‹จ)",
"expected_command": "guard = Guard().use(ToxicLanguage(threshold=0.5, on_fail='exception'))",
"grading_conditions": ["Guard()", "ToxicLanguage", "threshold=0.5", "on_fail='exception'"],
"hint": "`Guard().use(๊ฒ€์ฆ๊ธฐ(ํŒŒ๋ผ๋ฏธํ„ฐ))` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "์ด Guard๋กœ LLM์„ ๊ฐ์‹ธ๋ฉด LLM ์‘๋‹ต์ด ์œ ํ•ด์„ฑ ์ž„๊ณ„๊ฐ’์„ ์ดˆ๊ณผํ•  ๋•Œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. `on_fail='reask'`๋กœ ๋ฐ”๊พธ๋ฉด LLM์—๊ฒŒ ์ž๋™์œผ๋กœ ์žฌ์ž‘์„ฑ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "Guard().use(๊ฒ€์ฆ๊ธฐ) ํŒจํ„ด์œผ๋กœ LLM ์ถœ๋ ฅ์— ์•ˆ์ „์žฅ์น˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. on_fail='exception'์€ ์ฐจ๋‹จ, 'reask'๋Š” ์ž๋™ ์žฌ์ž‘์„ฑ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-20",
"title": "[๋‹จ๊ณ„ 20] Airflow โ€” ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ชป ๋†“์น˜๋Š” ๋ฌธ์ œ",
"task_description": "**์ƒํ™ฉ**\n๋งค์ฃผ ์ƒˆ ๋ฌธ์„œ๊ฐ€ ์ถ”๊ฐ€๋˜๋Š”๋ฐ, ์žฌ์ธ๋ฑ์‹ฑ์„ ์žŠ์–ด๋ฒ„๋ ค ์ฑ—๋ด‡์ด ์˜ค๋ž˜๋œ ์ •๋ณด๋กœ๋งŒ ๋‹ตํ•ฉ๋‹ˆ๋‹ค. ํŒ€์›์ด \"์™œ ์ด ๋‚ด์šฉ์ด ๊ฒ€์ƒ‰์ด ์•ˆ ๋ผ์š”?\"๋ผ๊ณ  ๋ฌผ์–ด๋ดค๋Š”๋ฐ, 2์ฃผ ์ „์— ์ถ”๊ฐ€๋œ ๋ฌธ์„œ์˜€์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์žฌ์ธ๋ฑ์‹ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ๋น ๋œจ๋ฆฌ๋Š” ๋‚ ์ด ์ƒ๊น๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`run_reindex` ํ•จ์ˆ˜๋ฅผ ๋งค์ผ ์ž์ •์— ์ž๋™ ์‹คํ–‰ํ•˜๋Š” Airflow DAG๋ฅผ ์ •์˜ํ•˜์„ธ์š”.\n- DAG ID: `daily_reindex`, ์Šค์ผ€์ค„: `@daily`, start_date: `datetime(2024, 1, 1)`",
"expected_command": "with DAG('daily_reindex', schedule='@daily', start_date=datetime(2024, 1, 1)) as dag:",
"grading_conditions": ["DAG", "daily_reindex", "schedule='@daily'", "start_date", "datetime"],
"hint": "`with DAG('id', schedule='@daily', start_date=datetime(...)) as dag:` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`@daily`๋Š” ๋งค์ผ ์ž์ •์— ์‹คํ–‰ํ•˜๋Š” Airflow ๋‚ด์žฅ ์Šค์ผ€์ค„ ํ‘œํ˜„์‹์ž…๋‹ˆ๋‹ค. ์ด DAG๊ฐ€ ์žˆ์œผ๋ฉด ๋‹ด๋‹น์ž๊ฐ€ ์—†๋Š” ์ฃผ๋ง์—๋„ ์ž๋™์œผ๋กœ ์žฌ์ธ๋ฑ์‹ฑ๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "@daily ์Šค์ผ€์ค„ ํ‘œํ˜„์‹ ํ•˜๋‚˜๋กœ ๋งค์ผ ์ž์ • ์ž๋™ ์‹คํ–‰์ด ๋ฉ๋‹ˆ๋‹ค. ์ˆ˜๋™ ์‹คํ–‰์€ ํ•ญ์ƒ ๋น ๋œจ๋ฆฌ๋Š” ๋‚ ์ด ์ƒ๊น๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-21",
"title": "[๋‹จ๊ณ„ 21] Airflow PythonOperator โ€” ์žฌ์ธ๋ฑ์‹ฑ ํ•จ์ˆ˜ ๋“ฑ๋ก",
"task_description": "**์ƒํ™ฉ**\nDAG ๊ตฌ์กฐ๋Š” ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์‹ค์ œ๋กœ ์–ด๋–ค ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ• ์ง€ ์ง€์ •ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nAirflow DAG๋Š” \"์–ธ์ œ ์‹คํ–‰ํ• ์ง€\"๋งŒ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. \"๋ฌด์—‡์„ ์‹คํ–‰ํ• ์ง€\"๋Š” Operator๋กœ ๋“ฑ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`reindex` ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๋Š” PythonOperator๋ฅผ DAG ์•ˆ์— ๋“ฑ๋กํ•˜์„ธ์š”.\n- task_id: `'reindex'`, python_callable: `reindex`",
"expected_command": "task = PythonOperator(task_id='reindex', python_callable=reindex)",
"grading_conditions": ["PythonOperator", "task_id='reindex'", "python_callable=reindex"],
"hint": "`PythonOperator(task_id='...', python_callable=ํ•จ์ˆ˜)` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`python_callable`์— ํ•จ์ˆ˜ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ ํ˜ธ์ถœ(`reindex()`)์ด ์•„๋‹Œ ํ•จ์ˆ˜ ์ฐธ์กฐ(`reindex`)๋ฅผ ๋„˜๊ฒจ์•ผ Airflow๊ฐ€ ์Šค์ผ€์ค„์— ๋งž์ถฐ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "python_callable์—๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ(reindex())์ด ์•„๋‹Œ ํ•จ์ˆ˜ ์ฐธ์กฐ(reindex)๋ฅผ ๋„˜๊ฒจ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ด„ํ˜ธ์˜ ์œ ๋ฌด๊ฐ€ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-22",
"title": "[๋‹จ๊ณ„ 22] MLflow โ€” ์–ด๋–ค ์„ค์ •์ด ๋” ์ข‹์€์ง€ ๊ธฐ์–ต์ด ์•ˆ ๋‚จ",
"task_description": "**์ƒํ™ฉ**\nchunk_size๋ฅผ 500์œผ๋กœ ํ–ˆ์„ ๋•Œ์™€ 1000์œผ๋กœ ํ–ˆ์„ ๋•Œ ๋‹ต๋ณ€ ํ’ˆ์งˆ์ด ๋‹ฌ๋ž์Šต๋‹ˆ๋‹ค. ๋ฉฐ์น  ํ›„ ๋‹ค์‹œ ๋น„๊ตํ•˜๋ ค๋‹ˆ \"์–ด๋–ค ์„ค์ •์œผ๋กœ ์–ด๋–ค ์ ์ˆ˜๊ฐ€ ๋‚˜์™”๋”๋ผ?\"๊ฐ€ ๊ธฐ์–ต์ด ์•ˆ ๋‚ฉ๋‹ˆ๋‹ค. ๋…ธ์…˜์— ์ˆ˜๋™์œผ๋กœ ๊ธฐ๋กํ–ˆ์ง€๋งŒ ๋ˆ„๋ฝ์ด ๋งŽ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nํŒŒ๋ผ๋ฏธํ„ฐ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ธฐ๋กํ•˜๋ฉด ๋น ๋œจ๋ฆฌ๊ฑฐ๋‚˜ ์žŠ์–ด๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nMLflow๋กœ ์‹คํ—˜์„ ๊ธฐ๋กํ•˜์„ธ์š”.\n- run ์ด๋ฆ„: `'rag-chunk500-k4'`\n- ํŒŒ๋ผ๋ฏธํ„ฐ: `chunk_size=500`, `k=4`\n- ๋ฉ”ํŠธ๋ฆญ: `answer_relevance=0.87`",
"expected_command": "with mlflow.start_run(run_name='rag-chunk500-k4'):\n mlflow.log_param('chunk_size', 500)\n mlflow.log_param('k', 4)\n mlflow.log_metric('answer_relevance', 0.87)",
"grading_conditions": ["mlflow.start_run", "log_param", "chunk_size", "log_metric", "answer_relevance"],
"hint": "`with mlflow.start_run(run_name='...'):`๋กœ ๋ธ”๋ก์„ ์—ด๊ณ , ์•ˆ์—์„œ `log_param`, `log_metric`์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`mlflow ui` ๋ช…๋ น์œผ๋กœ ์‹คํ—˜ ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์—ด๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์ฐจํŠธ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™ ๊ธฐ๋ก ์—†์ด ๋ชจ๋“  ์‹คํ—˜์ด ์ž๋™ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "mlflow.start_run() context manager๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋ฉ”ํŠธ๋ฆญ์„ ๊ธฐ๋กํ•˜๋ฉด ๋‚˜์ค‘์— mlflow ui์—์„œ ์‹คํ—˜์„ ์‹œ๊ฐ์ ์œผ๋กœ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-23",
"title": "[๋‹จ๊ณ„ 23] MCP โ€” Claude์—์„œ ๋ฐ”๋กœ ๋‚ด ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ ค๋ฉด",
"task_description": "**์ƒํ™ฉ**\nClaude Desktop์œผ๋กœ ๋Œ€ํ™” ์ค‘ \"์šฐ๋ฆฌ ๋‚ด๋ถ€ ๋ฌธ์„œ์— ์ด ๋‚ด์šฉ์ด ์žˆ์—ˆ๋Š”๋ฐ...\"๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค ๋•Œ๋งˆ๋‹ค Streamlit ์•ฑ์œผ๋กœ ์ด๋™ํ•ด์„œ ๊ฒ€์ƒ‰ํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ ๋ณต์‚ฌํ•ด Claude์— ๋ถ™์—ฌ๋„ฃ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nClaude๊ฐ€ ๋‚ด RAG ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ณ„๋„ ์•ฑ์„ ์™•๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nMCP ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด Claude์—๊ฒŒ `search_docs` ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”.\n- ์„œ๋ฒ„๋ช…: `'rag-server'`\n- ๋„๊ตฌ ํ•จ์ˆ˜: `search_docs(query: str)` โ†’ `retriever.invoke(query)`๋กœ ๊ฒ€์ƒ‰ ํ›„ page_content๋ฅผ ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ํ•ฉ์ณ ๋ฐ˜ํ™˜",
"expected_command": "mcp = FastMCP('rag-server')\n\n@mcp.tool()\ndef search_docs(query: str) -> str:\n results = retriever.invoke(query)\n return '\\n'.join(r.page_content for r in results)",
"grading_conditions": ["FastMCP", "@mcp.tool()", "def search_docs", "query: str", "retriever.invoke"],
"hint": "`FastMCP('์„œ๋ฒ„๋ช…')`์œผ๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ , `@mcp.tool()`์„ ํ•จ์ˆ˜ ์œ„์— ๋ถ™์ž…๋‹ˆ๋‹ค.",
"explanation": "`@mcp.tool()` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ๋ถ™์œผ๋ฉด Claude๋Š” ๋Œ€ํ™” ์ค‘ ์ด ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•จ์ˆ˜ docstring์ด Claude์—๊ฒŒ \"์–ธ์ œ ์ด ๋„๊ตฌ๋ฅผ ์“ฐ๋Š”๊ฐ€\"๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ์„ค๋ช…์ด ๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "@mcp.tool() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ํ•˜๋‚˜๋กœ ํŒŒ์ด์ฌ ํ•จ์ˆ˜๊ฐ€ Claude๊ฐ€ ๋Œ€ํ™” ์ค‘ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋„๊ตฌ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-24",
"title": "[๋‹จ๊ณ„ 24] MCP ์„œ๋ฒ„ ์‹คํ–‰ โ€” Claude Desktop์— ์—ฐ๊ฒฐ",
"task_description": "**์ƒํ™ฉ**\nMCP ์„œ๋ฒ„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋Š”๋ฐ, Claude Desktop์ด ์–ด๋–ป๊ฒŒ ์ด ์„œ๋ฒ„๋ฅผ ์ฐพ๊ณ  ์‹คํ–‰ํ•˜๋Š”์ง€ ๋ชจ๋ฆ…๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nClaude Desktop์€ stdio(ํ‘œ์ค€ ์ž…์ถœ๋ ฅ)๋กœ MCP ์„œ๋ฒ„ ํ”„๋กœ์„ธ์Šค์™€ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅธ transport๋ฅผ ์ง€์ •ํ•ด์•ผ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`mcp` ์ธ์Šคํ„ด์Šค๋ฅผ stdio ๋ชจ๋“œ๋กœ ์‹คํ–‰ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.\n\n> Claude Desktop ์„ค์ • ํŒŒ์ผ์— ์ด ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๋ช…๋ น์–ด๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ์ž๋™ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.",
"expected_command": "mcp.run(transport='stdio')",
"grading_conditions": ["mcp.run", "transport='stdio'"],
"hint": "`mcp.run(transport='...')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ์€ stdio, HTTP ์„œ๋ฒ„๋Š” sse์ž…๋‹ˆ๋‹ค.",
"explanation": "Claude Desktop์€ `claude_desktop_config.json`์— ๋“ฑ๋ก๋œ ๋ช…๋ น์–ด๋กœ MCP ์„œ๋ฒ„ ํ”„๋กœ์„ธ์Šค๋ฅผ ์‹คํ–‰ํ•˜๊ณ  stdio๋กœ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๋ฉด ๋Œ€ํ™” ์ค‘ `search_docs` ๋„๊ตฌ๊ฐ€ ์ž๋™์œผ๋กœ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "transport='stdio'๊ฐ€ Claude Desktop ์—ฐ๊ฒฐ, 'sse'๊ฐ€ HTTP ์„œ๋ฒ„ ์—ฐ๊ฒฐ์ž…๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ ๋ฐฉ์‹์„ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-lab-25",
"title": "[๋ณด๋„ˆ์Šค] Gradio โ€” Streamlit ์—†์ด ์ฑ—๋ด‡ UI ๋งŒ๋“ค๊ธฐ",
"task_description": "**์ƒํ™ฉ**\nStreamlit ์ฑ—๋ด‡์— ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต(ํƒ€์ดํ•‘ ํšจ๊ณผ)์„ ๊ตฌํ˜„ํ•˜๋ ค๋‹ˆ ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค. `st.write_stream`๊ณผ generator ํ•จ์ˆ˜ ์„ค์ •์ด ๋ฒˆ๊ฑฐ๋กญ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nStreamlit์€ rerun ๊ธฐ๋ฐ˜์ด๋ผ ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ UI ๊ตฌํ˜„์ด ์ƒ๋Œ€์ ์œผ๋กœ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nGradio์˜ `ChatInterface`๋Š” generator ํ•จ์ˆ˜๋ฅผ ๋„˜๊ธฐ๋ฉด ์ž๋™์œผ๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ UI๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.\n\n`chat(message, history)` ํ•จ์ˆ˜๋ฅผ `gr.ChatInterface`์— ์—ฐ๊ฒฐํ•˜๊ณ , ์ œ๋ชฉ `'RAG ์ฑ—๋ด‡'`์„ ์„ค์ •ํ•œ ๋’ค ์‹คํ–‰ํ•˜์„ธ์š”.",
"expected_command": "demo = gr.ChatInterface(\n fn=chat,\n title='RAG ์ฑ—๋ด‡'\n)\ndemo.launch()",
"grading_conditions": ["gr.ChatInterface", "fn=chat", "demo.launch()"],
"hint": "`gr.ChatInterface(fn=ํ•จ์ˆ˜, title='์ œ๋ชฉ')`์œผ๋กœ ๋งŒ๋“ค๊ณ  `.launch()`๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`gr.ChatInterface`๋Š” `fn`์ด `(message: str, history: list) -> str` ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋”ฐ๋ฅด๋ฉด ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. generator ํ•จ์ˆ˜๋กœ ๋ฐ”๊พธ๋ฉด ํ† ํฐ ๋‹จ์œ„ ์ŠคํŠธ๋ฆฌ๋ฐ์ด ๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "gr.ChatInterface๋Š” ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ์ฑ—๋ด‡์„ ํ•จ์ˆ˜ ํ•˜๋‚˜๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
},
{
"id": "docs-chroma-1",
"title": "[Chroma 1] FAISS โ†’ Chroma ๊ต์ฒด โ€” ์žฌ์‹œ์ž‘ํ•ด๋„ ๋ฐ์ดํ„ฐ ์œ ์ง€",
"task_description": "**์ƒํ™ฉ**\n์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ–ˆ๋”๋‹ˆ VectorStore๊ฐ€ ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. ๋งค๋ฒˆ `ollama pull`์„ ๋‹ค์‹œ ํ•  ์ˆ˜๋Š” ์—†์–ด `save_local()`์„ ํ˜ธ์ถœํ•˜๋„๋ก ํ–ˆ๋Š”๋ฐ, ํ˜ธ์ถœํ•˜๋Š” ๊ฑธ ํ•œ ๋ฒˆ ์žŠ์–ด๋ฒ„๋ ธ๋”๋‹ˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ ์•„๊ฐ”์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nFAISS๋Š” ์ˆ˜๋™์œผ๋กœ `save_local()`์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์žŠ์–ด๋ฒ„๋ฆฌ๋Š” ์ˆœ๊ฐ„ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nChroma๋Š” `persist_directory`๋งŒ ์ง€์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.\n\n`chunks`์™€ `embeddings`๋กœ Chroma VectorStore๋ฅผ ๋งŒ๋“ค๊ณ , `./chroma_db`์— ์ €์žฅํ•˜์„ธ์š”. ๊ฒฐ๊ณผ๋ฅผ `db`์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "db = Chroma.from_documents(chunks, embeddings, persist_directory='./chroma_db')",
"grading_conditions": ["Chroma.from_documents", "chunks", "embeddings", "persist_directory='./chroma_db'"],
"hint": "`Chroma.from_documents(๋ฌธ์„œ, ์ž„๋ฒ ๋”ฉ, persist_directory='๊ฒฝ๋กœ')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`persist_directory`๋ฅผ ์ง€์ •ํ•˜๋ฉด `save_local()` ํ˜ธ์ถœ ์—†์ด ์ž๋™์œผ๋กœ SQLite ๊ธฐ๋ฐ˜ ๋กœ์ปฌ DB์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. `Chroma(persist_directory='./chroma_db', embedding_function=embeddings)`๋กœ ์žฌ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "Chroma๋Š” persist_directory ์ง€์ •๋งŒ์œผ๋กœ save_local() ์—†์ด ์ž๋™ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. FAISS์™€ ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-chroma-2",
"title": "[Chroma 2] ๊ธฐ์กด Chroma DB ์žฌ๋กœ๋“œ โ€” ์ž„๋ฒ ๋”ฉ ์žฌ๊ณ„์‚ฐ ์—†์ด",
"task_description": "**์ƒํ™ฉ**\n์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. `./chroma_db` ํด๋”์— ์ด๋ฏธ ์ธ๋ฑ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”๋ฐ, ๋‹ค์‹œ `Chroma.from_documents()`๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ž„๋ฒ ๋”ฉ์„ ๋˜ ๊ณ„์‚ฐํ•ด ๋ช‡ ๋ถ„์ด ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์ด๋ฏธ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด `from_documents`๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ๋กœ๋“œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`./chroma_db`์— ์ €์žฅ๋œ ๊ธฐ์กด Chroma DB๋ฅผ `embeddings` ๋ชจ๋ธ๋กœ ์žฌ๋กœ๋“œํ•˜์„ธ์š”. ๊ฒฐ๊ณผ๋ฅผ `db`์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "db = Chroma(persist_directory='./chroma_db', embedding_function=embeddings)",
"grading_conditions": ["Chroma(", "persist_directory='./chroma_db'", "embedding_function=embeddings"],
"hint": "`Chroma(persist_directory='๊ฒฝ๋กœ', embedding_function=์ž„๋ฒ ๋”ฉ๋ชจ๋ธ)` ํŒจํ„ด์ž…๋‹ˆ๋‹ค. `from_documents`์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.",
"explanation": "`Chroma()`๋Š” ๊ธฐ์กด DB๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. `Chroma.from_documents()`๋Š” ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘ ์‹œ์—๋Š” `Chroma()`๋ฅผ ์จ์•ผ ์ž„๋ฒ ๋”ฉ ์žฌ๊ณ„์‚ฐ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "Chroma()๋Š” ๊ธฐ์กด DB ์žฌ๋กœ๋“œ, Chroma.from_documents()๋Š” ์‹ ๊ทœ ์ƒ์„ฑ์ž…๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘ ์‹œ ์–ด๋А ์ชฝ์„ ์“ฐ๋Š”์ง€๊ฐ€ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-embed-1",
"title": "[sentence-transformers] OpenAI ์—†์ด ๋กœ์ปฌ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ",
"task_description": "**์ƒํ™ฉ**\nRAG๋ฅผ ๊ตฌ์„ฑํ–ˆ๋Š”๋ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ์— OpenAI API๋ฅผ ์“ฐ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๊ฐ€ ๋งŽ์•„์ง€์ž ์ž„๋ฒ ๋”ฉ ๋น„์šฉ์ด ์˜ˆ์ƒ๋ณด๋‹ค ๋งŽ์ด ๋‚˜์™”์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nOpenAI ์ž„๋ฒ ๋”ฉ์€ ํ† ํฐ๋‹น ๋น„์šฉ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์šฉ๋Ÿ‰ ๋ฌธ์„œ๋‚˜ ๋ฐ˜๋ณต ์‹คํ—˜์— ๋ถ€๋‹ด์ด ๋ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nHuggingFace์˜ `all-MiniLM-L6-v2` ๋ชจ๋ธ๋กœ ์™„์ „ ๋ฌด๋ฃŒ ๋กœ์ปฌ ์ž„๋ฒ ๋”ฉ์„ ์„ค์ •ํ•˜์„ธ์š”. ๊ฒฐ๊ณผ๋ฅผ `embeddings` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "embeddings = HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2')",
"grading_conditions": ["HuggingFaceEmbeddings", "model_name='sentence-transformers/all-MiniLM-L6-v2'"],
"hint": "`HuggingFaceEmbeddings(model_name='...')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์ฒซ ์‹คํ–‰ ์‹œ ๋ชจ๋ธ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "์ดํ›„ `Chroma.from_documents(chunks, embeddings)` ๋˜๋Š” `FAISS.from_documents(chunks, embeddings)`์— ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. OpenAI Embeddings์™€ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋™์ผํ•ด ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ํ•œ ์ค„์ž…๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "HuggingFaceEmbeddings๋Š” OpenAI Embeddings์™€ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋™์ผํ•ด model_name ํ•œ ์ค„๋งŒ ๋ฐ”๊พธ๋ฉด ๋ฌด๋ฃŒ ๋กœ์ปฌ ์ž„๋ฒ ๋”ฉ์œผ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-ragas-1",
"title": "[Ragas 1] ํ‰๊ฐ€ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์„ฑ",
"task_description": "**์ƒํ™ฉ**\nMLflow๋กœ `chunk_size`์™€ `k`๋ฅผ ๋ฐ”๊ฟ”๊ฐ€๋ฉฐ ์‹คํ—˜ํ–ˆ๋Š”๋ฐ, `answer_relevance=0.87`์ด๋ผ๊ณ  ๊ธฐ๋กํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด ์ˆซ์ž๋ฅผ ์–ด๋–ป๊ฒŒ ์ธก์ •ํ–ˆ๋Š”์ง€ ๊ธฐ์ค€์ด ์—†์–ด ๋‹ค์Œ ์‹คํ—˜๊ณผ ๋น„๊ต๊ฐ€ ์•ˆ ๋ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n\"์ข‹์€ RAG\"์˜ ๊ธฐ์ค€์ด ์—†์Šต๋‹ˆ๋‹ค. ์ˆ˜์น˜ํ™”๋œ ํ‰๊ฐ€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nRagas ํ‰๊ฐ€์šฉ Dataset์„ ๊ตฌ์„ฑํ•˜์„ธ์š”. ์•„๋ž˜ ๋ฐ์ดํ„ฐ๋ฅผ `Dataset.from_dict()`๋กœ ๋งŒ๋“ค์–ด `eval_dataset`์— ์ €์žฅํ•˜์„ธ์š”.\n\n```python\ndata = {\n 'question': ['LangChain์ด ๋ญ์•ผ?'],\n 'answer': ['LangChain์€ LLM ์•ฑ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.'],\n 'contexts': [['LangChain์€ LLM ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.']],\n 'ground_truth': ['LangChain์€ LLM ์•ฑ ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.']\n}\n```",
"expected_command": "eval_dataset = Dataset.from_dict(data)",
"grading_conditions": ["Dataset.from_dict", "data"],
"hint": "`from datasets import Dataset` ํ›„ `Dataset.from_dict(๋”•์…”๋„ˆ๋ฆฌ)` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "Ragas๋Š” HuggingFace `datasets` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Dataset ํ˜•์‹์„ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค. `question`, `answer`, `contexts`, `ground_truth` 4๊ฐœ ํ‚ค๊ฐ€ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "Ragas๋Š” question, answer, contexts, ground_truth 4๊ฐœ ํ‚ค๋ฅผ ๊ฐ€์ง„ Dataset์„ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด ๊ตฌ์กฐ๊ฐ€ RAG ํ‰๊ฐ€์˜ ํ‘œ์ค€ ํ˜•์‹์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-ragas-2",
"title": "[Ragas 2] RAG ํ’ˆ์งˆ ์ž๋™ ํ‰๊ฐ€ ์‹คํ–‰",
"task_description": "**์ƒํ™ฉ**\nํ‰๊ฐ€ ๋ฐ์ดํ„ฐ์…‹์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์‹ค์ œ๋กœ Faithfulness, Answer Relevancy, Context Precision์„ ์ž๋™์œผ๋กœ ์ธก์ •ํ•ด๋ด…๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`eval_dataset`์— ๋Œ€ํ•ด 3๊ฐ€์ง€ ๋ฉ”ํŠธ๋ฆญ์œผ๋กœ Ragas ํ‰๊ฐ€๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.\n\n- ๋ฉ”ํŠธ๋ฆญ: `faithfulness`, `answer_relevancy`, `context_precision`\n- ๊ฒฐ๊ณผ๋ฅผ `result`์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "result = evaluate(eval_dataset, metrics=[faithfulness, answer_relevancy, context_precision])",
"grading_conditions": ["evaluate", "eval_dataset", "faithfulness", "answer_relevancy", "context_precision"],
"hint": "`evaluate(dataset, metrics=[...])` ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ๋ฉ”ํŠธ๋ฆญ์€ ๋ฆฌ์ŠคํŠธ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`result`์—๋Š” ๊ฐ ๋ฉ”ํŠธ๋ฆญ ์ ์ˆ˜๊ฐ€ ๋‹ด๊น๋‹ˆ๋‹ค. `result.to_pandas()`๋กœ DataFrame ๋ณ€ํ™˜ ํ›„ `mlflow.log_metrics(result.to_pandas().mean().to_dict())`๋กœ MLflow์— ๊ธฐ๋กํ•˜๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„ ํ’ˆ์งˆ ๋น„๊ต๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "result.to_pandas()๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค mlflow.log_metrics()์— ๋„˜๊ธฐ๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ ์‹คํ—˜๊ณผ ํ’ˆ์งˆ ๋ฉ”ํŠธ๋ฆญ์„ ํ•จ๊ป˜ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
},
{
"id": "docs-graph-1",
"title": "[LangGraph 1] StateGraph ๊ธฐ๋ณธ ๊ตฌ์กฐ",
"task_description": "**์ƒํ™ฉ**\nRAG ์ฒด์ธ์„ ์“ฐ๋‹ค ๋ณด๋‹ˆ \"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ๋‹ค์‹œ ์‹œ๋„\", \"ํ’ˆ์งˆ์ด ๋‚ฎ์œผ๋ฉด ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋กœ\" ๊ฐ™์€ ์กฐ๊ฑด ๋ถ„๊ธฐ๊ฐ€ ํ•„์š”ํ•ด์กŒ์Šต๋‹ˆ๋‹ค. LCEL ์ฒด์ธ์€ Aโ†’Bโ†’C ์„ ํ˜•์ด๋ผ ๋ถ„๊ธฐ๋ฅผ ํ‘œํ˜„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๋‹จ์ˆœ ์ฒด์ธ์œผ๋กœ๋Š” ๋ฃจํ”„๋‚˜ ์กฐ๊ฑด ๋ถ„๊ธฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nLangGraph๋กœ 2๊ฐœ ๋…ธ๋“œ(`retrieve`, `generate`)๋ฅผ ๊ฐ€์ง„ StateGraph๋ฅผ ๋งŒ๋“œ์„ธ์š”.\n\n```python\nclass State(TypedDict):\n question: str\n answer: str\n```\n\n- ๋…ธ๋“œ ์ถ”๊ฐ€: `retrieve`, `generate`\n- ์ง„์ž…์ : `retrieve`\n- ์—ฃ์ง€: `retrieve โ†’ generate โ†’ END`\n- ์ปดํŒŒ์ผํ•ด์„œ `app`์— ์ €์žฅ",
"expected_command": "graph = StateGraph(State)\ngraph.add_node('retrieve', retrieve)\ngraph.add_node('generate', generate)\ngraph.set_entry_point('retrieve')\ngraph.add_edge('retrieve', 'generate')\ngraph.add_edge('generate', END)\napp = graph.compile()",
"grading_conditions": ["StateGraph", "add_node", "set_entry_point", "add_edge", "END", "graph.compile()"],
"hint": "StateGraph โ†’ add_node โ†’ set_entry_point โ†’ add_edge โ†’ compile() ์ˆœ์„œ์ž…๋‹ˆ๋‹ค.",
"explanation": "`graph.compile()`๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์•ฑ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. `app.invoke({'question': '์งˆ๋ฌธ'})`์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉด retrieve ๋…ธ๋“œ โ†’ generate ๋…ธ๋“œ ์ˆœ์„œ๋กœ State๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "LangGraph๋Š” compile() ํ›„ invoke()๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ฒด์ธ์˜ invoke()์™€ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค์ด์ง€๋งŒ, ๋‚ด๋ถ€์ ์œผ๋กœ ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-graph-2",
"title": "[LangGraph 2] conditional_edges โ€” ์กฐ๊ฑด ๋ถ„๊ธฐ",
"task_description": "**์ƒํ™ฉ**\ngenerate ๋…ธ๋“œ์—์„œ LLM์ด '๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค'๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด retrieve๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ `add_edge`๋Š” ํ•ญ์ƒ ๊ณ ์ •๋œ ๋‹ค์Œ ๋…ธ๋“œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๋…ธ๋“œ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๋™์ ์œผ๋กœ ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`judge` ๋…ธ๋“œ์˜ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋ถ„๊ธฐํ•˜๋Š” conditional_edges๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.\n\n- router ํ•จ์ˆ˜: `should_retry` (์ด๋ฏธ ์ •์˜๋จ)\n- `'retry'` โ†’ `'retrieve'` ๋…ธ๋“œ๋กœ\n- `'done'` โ†’ `END`๋กœ",
"expected_command": "graph.add_conditional_edges('judge', should_retry, {'retry': 'retrieve', 'done': END})",
"grading_conditions": ["add_conditional_edges", "'judge'", "should_retry", "'retry'", "retrieve", "END"],
"hint": "`graph.add_conditional_edges('๋…ธ๋“œ', ๋ผ์šฐํ„ฐํ•จ์ˆ˜, {'๋ฐ˜ํ™˜๊ฐ’': '๋‹ค์Œ๋…ธ๋“œ', ...})` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`should_retry` ํ•จ์ˆ˜๊ฐ€ State๋ฅผ ๋ฐ›์•„ `'retry'` ๋˜๋Š” `'done'`์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด, ๊ฐ๊ฐ `retrieve` ๋…ธ๋“œ ๋˜๋Š” `END`๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ReAct ์—์ด์ „ํŠธ์˜ \"ํ–‰๋™ โ†’ ๊ด€์ฐฐ โ†’ ํŒ๋‹จ\" ๋ฃจํ”„์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "add_conditional_edges๋กœ ๋ผ์šฐํ„ฐ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์— ๋”ฐ๋ผ ๋‹ค์Œ ๋…ธ๋“œ๋ฅผ ๋™์ ์œผ๋กœ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์ด LangGraph ์—์ด์ „ํŠธ์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-litellm-1",
"title": "[LiteLLM] ๋ชจ๋ธ๋ช… ํ•˜๋‚˜๋กœ Ollama โ†” OpenAI ์ „ํ™˜",
"task_description": "**์ƒํ™ฉ**\n๋กœ์ปฌ์—์„œ Ollama๋กœ ๊ฐœ๋ฐœํ–ˆ๋Š”๋ฐ, ํ”„๋กœ๋•์…˜์—์„œ๋Š” OpenAI๋ฅผ ์จ์•ผ ํ•ฉ๋‹ˆ๋‹ค. `OllamaLLM`์„ `ChatOpenAI`๋กœ ๋ฐ”๊พธ๋ ค๋‹ˆ import, ํด๋ž˜์Šค๋ช…, API ํ˜•์‹์ด ๋ชจ๋‘ ๋‹ฌ๋ผ ์ฝ”๋“œ๋ฅผ ๋งŽ์ด ๊ณ ์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nLLM ํ”„๋กœ๋ฐ”์ด๋”๋งˆ๋‹ค SDK๊ฐ€ ๋‹ฌ๋ผ ๊ต์ฒด ์‹œ ์ฝ”๋“œ ์ˆ˜์ •์ด ๋งŽ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nLiteLLM์˜ `completion()`์œผ๋กœ Ollama `llama3.2`์— `'์•ˆ๋…•'`์„ ๋ณด๋‚ด๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”. ๋‚˜์ค‘์— `'gpt-4o-mini'`๋กœ ๋ฐ”๊ฟ€ ๋•Œ model ํŒŒ๋ผ๋ฏธํ„ฐ ํ•˜๋‚˜๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.",
"expected_command": "response = completion(model='ollama/llama3.2', messages=[{'role': 'user', 'content': '์•ˆ๋…•'}])",
"grading_conditions": ["completion", "model='ollama/llama3.2'", "messages", "role", "user"],
"hint": "`completion(model='ํ”„๋กœ๋ฐ”์ด๋”/๋ชจ๋ธ๋ช…', messages=[...])` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "์ดํ›„ `model='gpt-4o-mini'`๋กœ๋งŒ ๋ฐ”๊พธ๋ฉด OpenAI๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. `response.choices[0].message.content`๋กœ ์‘๋‹ต์„ ์ฝ๋Š” ๋ฐฉ์‹๋„ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "model='ํ”„๋กœ๋ฐ”์ด๋”/๋ชจ๋ธ๋ช…' ํ˜•์‹์œผ๋กœ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด LLM์„ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด LiteLLM์˜ ํ•ต์‹ฌ ๊ฐ€์น˜์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-chainlit-1",
"title": "[Chainlit] ์ŠคํŠธ๋ฆฌ๋ฐ ์ฑ—๋ด‡ โ€” Streamlit๋ณด๋‹ค ๊ฐ„๊ฒฐํ•˜๊ฒŒ",
"task_description": "**์ƒํ™ฉ**\nStreamlit ์ฑ—๋ด‡์— ์ŠคํŠธ๋ฆฌ๋ฐ(ํƒ€์ดํ•‘ ํšจ๊ณผ)์„ ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ, `st.write_stream`, generator ํ•จ์ˆ˜, `st.empty()` ๋“ฑ ์—ฌ๋Ÿฌ ๊ฐœ๋…์ด ์–ฝํ˜€ ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nStreamlit์€ rerun ๊ธฐ๋ฐ˜์ด๋ผ ์ŠคํŠธ๋ฆฌ๋ฐ UI ๊ตฌํ˜„์ด ์žฅํ™ฉํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nChainlit์œผ๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ์ฑ—๋ด‡์„ ๋งŒ๋“œ์„ธ์š”.\n\n`@cl.on_message` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ณ , `cl.Message(content='')`๋ฅผ ๋งŒ๋“  ๋’ค `chain.astream()`์œผ๋กœ ํ† ํฐ์„ ๋ฐ›์•„ `msg.stream_token(chunk)`์œผ๋กœ ์ „์†กํ•˜์„ธ์š”.",
"expected_command": "@cl.on_message\nasync def main(message: cl.Message):\n msg = cl.Message(content='')\n async for chunk in chain.astream({'question': message.content}):\n await msg.stream_token(chunk)\n await msg.send()",
"grading_conditions": ["@cl.on_message", "cl.Message", "chain.astream", "msg.stream_token", "msg.send()"],
"hint": "`@cl.on_message` โ†’ `cl.Message(content='')` ์ƒ์„ฑ โ†’ `astream`์œผ๋กœ ํ† ํฐ ๋ฐ›๊ธฐ โ†’ `stream_token` โ†’ `send()` ์ˆœ์„œ์ž…๋‹ˆ๋‹ค.",
"explanation": "Chainlit์—์„œ `astream()`๊ณผ `stream_token()`์„ ์กฐํ•ฉํ•˜๋ฉด ํ† ํฐ ๋‹จ์œ„ ์ŠคํŠธ๋ฆฌ๋ฐ์ด ๋ฉ๋‹ˆ๋‹ค. Streamlit๋ณด๋‹ค ํ›จ์”ฌ ์ ์€ ์ฝ”๋“œ๋กœ ๋™์ผ ํšจ๊ณผ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. `chainlit run app.py`๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "astream()์œผ๋กœ ํ† ํฐ์„ ๋ฐ›์•„ stream_token()์œผ๋กœ ์ „์†กํ•˜๋Š” ํŒจํ„ด์ด Chainlit ์ŠคํŠธ๋ฆฌ๋ฐ์˜ ์ „๋ถ€์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-redis-1",
"title": "[Redis] ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ์˜์†ํ™” โ€” ์žฌ์‹œ์ž‘ํ•ด๋„ ๊ธฐ์–ต",
"task_description": "**์ƒํ™ฉ**\n```python\nstore = {} # ์ธ๋ฉ”๋ชจ๋ฆฌ\ndef get_session_history(session_id):\n if session_id not in store:\n store[session_id] = ChatMessageHistory()\n return store[session_id]\n```\n์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ•˜๋‹ˆ ๋ชจ๋“  ์‚ฌ์šฉ์ž์˜ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. `store` ๋”•์…”๋„ˆ๋ฆฌ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์ธ๋ฉ”๋ชจ๋ฆฌ ํžˆ์Šคํ† ๋ฆฌ๋Š” ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ ์‹œ ์†Œ๋ฉธํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`get_session_history` ํ•จ์ˆ˜๋ฅผ `RedisChatMessageHistory`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์„ธ์š”.\n- Redis URL: `'redis://localhost:6379'`",
"expected_command": "def get_session_history(session_id: str):\n return RedisChatMessageHistory(session_id=session_id, url='redis://localhost:6379')",
"grading_conditions": ["RedisChatMessageHistory", "session_id=session_id", "url='redis://localhost:6379'"],
"hint": "`RedisChatMessageHistory(session_id=session_id, url='redis://...')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "ํ•จ์ˆ˜ ๋‚ด๋ถ€๋ฅผ `RedisChatMessageHistory`๋กœ ๊ต์ฒดํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. `RunnableWithMessageHistory`๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ์ด ํ•จ์ˆ˜๋งŒ ๋ฐ”๊พธ๋ฉด ์˜์†์„ฑ์„ ์–ป์Šต๋‹ˆ๋‹ค. Redis ์„œ๋ฒ„๋Š” `docker run -d -p 6379:6379 redis`๋กœ ์‹คํ–‰ํ•˜์„ธ์š”.",
"difficulty": "easy",
"takeaway": "get_session_history ํ•จ์ˆ˜ ๋‚ด๋ถ€๋ฅผ RedisChatMessageHistory๋กœ ๊ต์ฒดํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ์žฌ์‹œ์ž‘์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-unstructured-1",
"title": "[Unstructured] PDFยทWord ๋“ฑ ์‹ค๋ฌด ๋ฌธ์„œ ํŒŒ์‹ฑ",
"task_description": "**์ƒํ™ฉ**\nDirectoryLoader๋กœ `./docs` ํด๋”๋ฅผ ๋กœ๋“œํ•˜๋ ค๋Š”๋ฐ, `.md` ํŒŒ์ผ์€ ์ž˜ ๋์ง€๋งŒ ํŒ€์—์„œ ๊ณต์œ ๋ฐ›์€ ๋ณด๊ณ ์„œ๋Š” `.pdf`, `.docx`, `.pptx`๊ฐ€ ์„ž์—ฌ ์žˆ์Šต๋‹ˆ๋‹ค. ํ™•์žฅ์ž๋งˆ๋‹ค ๋‹ค๋ฅธ ๋กœ๋”๋ฅผ ์จ์•ผ ํ•ด ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์กŒ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nํŒŒ์ผ ํ˜•์‹๋งˆ๋‹ค ๋‹ค๋ฅธ ๋กœ๋”๋ฅผ ์“ฐ๋ฉด ์ฝ”๋“œ๊ฐ€ ๋ถ„๊ธฐ๋ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`UnstructuredFileLoader`๋ฅผ `loader_cls`๋กœ ์ง€์ •ํ•ด `./docs` ํด๋”์˜ ๋ชจ๋“  ํŒŒ์ผ์„ ํ˜•์‹์— ์ƒ๊ด€์—†์ด ๋กœ๋“œํ•˜์„ธ์š”. ๊ฒฐ๊ณผ๋ฅผ `docs` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "loader = DirectoryLoader('./docs', glob='**/*', loader_cls=UnstructuredFileLoader)\ndocs = loader.load()",
"grading_conditions": ["DirectoryLoader", "./docs", "loader_cls=UnstructuredFileLoader", "loader.load()"],
"hint": "`DirectoryLoader('ํด๋”', glob='**/*', loader_cls=UnstructuredFileLoader)` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`UnstructuredFileLoader`๋Š” ํ™•์žฅ์ž๋ฅผ ์ž๋™ ๊ฐ์ง€ํ•ด ์ ์ ˆํ•œ ํŒŒ์„œ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. `.pdf`, `.docx`, `.pptx`, `.jpg` ๋“ฑ์„ ๋™์ผ ์ฝ”๋“œ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. PDF์—๋Š” `pip install unstructured[pdf]`๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "loader_cls=UnstructuredFileLoader ํ•˜๋‚˜๋กœ PDFยทWordยทPowerPoint ๋“ฑ ๋ชจ๋“  ๋ฌธ์„œ ํ˜•์‹์„ ๋™์ผํ•œ ์ฝ”๋“œ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-news-1",
"title": "[๋‰ด์Šค ์š”์•ฝ๊ธฐ 1] feedparser๋กœ RSS ํŒŒ์‹ฑ",
"task_description": "**ํ”„๋กœ์ ํŠธ: ๋‰ด์Šค ์ž๋™ ์š”์•ฝ๊ธฐ**\n๋งค์ผ ์•„์นจ ๋‰ด์Šค ์‚ฌ์ดํŠธ 5๊ฐœ๋ฅผ ์ง์ ‘ ์—ด์–ด ์ฝ์Šต๋‹ˆ๋‹ค. ์ž๋™ํ™”ํ•˜๊ณ  ์‹ถ์€๋ฐ ์‚ฌ์ดํŠธ๋งˆ๋‹ค HTML ๊ตฌ์กฐ๊ฐ€ ๋‹ฌ๋ผ ํฌ๋กค๋ง ์ฝ”๋“œ๊ฐ€ ๊ฐ๊ฐ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nHTML ํฌ๋กค๋ง์€ ์‚ฌ์ดํŠธ ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ฝ”๋“œ๊ฐ€ ๊นจ์ง‘๋‹ˆ๋‹ค. ๋‰ด์Šค ์‚ฌ์ดํŠธ ๋Œ€๋ถ€๋ถ„์ด RSS๋ผ๋Š” ํ‘œ์ค€ ํ˜•์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`feedparser`๋กœ BBC ๋‰ด์Šค RSS๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ , ์ตœ์‹  ๊ธฐ์‚ฌ 5๊ฐœ์˜ ์š”์•ฝ์„ `articles` ๋ฆฌ์ŠคํŠธ์— ์ €์žฅํ•˜์„ธ์š”.\n\nFeed URL: `https://feeds.bbci.co.uk/news/rss.xml`",
"expected_command": "import feedparser\nfeed = feedparser.parse('https://feeds.bbci.co.uk/news/rss.xml')\narticles = [entry.summary for entry in feed.entries[:5]]",
"grading_conditions": ["feedparser.parse", "feed.entries", "entry.summary", "articles"],
"hint": "`feedparser.parse(url)`๋กœ ํŒŒ์‹ฑํ•˜๋ฉด `feed.entries`์— ๊ธฐ์‚ฌ ๋ชฉ๋ก์ด ๋“ค์–ด์žˆ์Šต๋‹ˆ๋‹ค.",
"explanation": "RSS๋Š” XML ๊ธฐ๋ฐ˜ ํ‘œ์ค€ ํ˜•์‹์ด๋ผ ์–ด๋А ์‚ฌ์ดํŠธ๋“  `feed.entries`๋กœ ๋™์ผํ•˜๊ฒŒ ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค. HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "RSS๋Š” ์‚ฌ์ดํŠธ ๊ตฌ์กฐ์— ๊ด€๊ณ„์—†์ด feed.entries๋กœ ๋™์ผํ•˜๊ฒŒ ์ ‘๊ทผํ•˜๋Š” ํ‘œ์ค€ ํ˜•์‹์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-news-2",
"title": "[๋‰ด์Šค ์š”์•ฝ๊ธฐ 2] map_reduce โ€” ํ† ํฐ ์ดˆ๊ณผ ์—†์ด ์š”์•ฝ",
"task_description": "**์ƒํ™ฉ**\n๊ธฐ์‚ฌ 10๊ฐœ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ์š”์•ฝํ•˜๋ ค๊ณ  `stuff` ๋ฐฉ์‹์„ ์ผ๋”๋‹ˆ:\n```\nError: This model's maximum context length is 128000 tokens.\nYour message resulted in 180000 tokens.\n```\n\n**๋ฌธ์ œ**\n`stuff`๋Š” ๋ชจ๋“  ๋ฌธ์„œ๋ฅผ ํ•˜๋‚˜์˜ ํ”„๋กฌํ”„ํŠธ์— ๋„ฃ์–ด์„œ ๋ฌธ์„œ๊ฐ€ ๋งŽ์œผ๋ฉด ํ† ํฐ์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`map_reduce`๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ๊ฐ ๊ธฐ์‚ฌ๋ฅผ ๊ฐœ๋ณ„ ์š”์•ฝํ•œ ๋’ค ํ•ฉ์นฉ๋‹ˆ๋‹ค.\n\n`articles` โ†’ `Document` ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ ํ›„ `map_reduce` ์ฒด์ธ์œผ๋กœ ์š”์•ฝํ•˜๋Š” ์ฝ”๋“œ์˜ `???` ๋ถ€๋ถ„์„ ์ฑ„์šฐ์„ธ์š”.\n\n```python\ndocs = [Document(page_content=a) for a in articles]\nchain = ???\nresult = chain.invoke(docs)\n```",
"expected_command": "chain = load_summarize_chain(llm, chain_type='map_reduce')",
"grading_conditions": ["load_summarize_chain", "llm", "chain_type='map_reduce'"],
"hint": "`load_summarize_chain(llm, chain_type='...')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`map_reduce`๋Š” ๊ฐ ๊ธฐ์‚ฌ๋ฅผ ๊ฐœ๋ณ„ ์š”์•ฝ(map)ํ•˜๊ณ , ๊ฐœ๋ณ„ ์š”์•ฝ๋“ค์„ ๋‹ค์‹œ ํ•ฉ์ณ ์ตœ์ข… ์š”์•ฝ(reduce)์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์•„๋ฌด๋ฆฌ ๊ธฐ์‚ฌ๊ฐ€ ๋งŽ์•„๋„ ํ† ํฐ ์ดˆ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "map_reduce๋Š” ๊ฐ ๋ฌธ์„œ๋ฅผ ๊ฐœ๋ณ„ ์š”์•ฝ(map)ํ•œ ๋’ค ํ•ฉ์น˜๋Š”(reduce) ๋ฐฉ์‹์œผ๋กœ ํ† ํฐ ์ œํ•œ ์—†์ด ์—ฌ๋Ÿฌ ๋ฌธ์„œ๋ฅผ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-news-3",
"title": "[๋‰ด์Šค ์š”์•ฝ๊ธฐ 3] Airflow โ€” ์ž๋Š” ๋™์•ˆ๋„ ์ž๋™ ์‹คํ–‰",
"task_description": "**์ƒํ™ฉ**\nfeedparser + LangChain ์š”์•ฝ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š”๋ฐ, ๋งค์ผ ์•„์นจ ์ง์ ‘ ํ„ฐ๋ฏธ๋„์— `python news_summary.py`๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ๋ง์— ๊นœ๋นกํ•˜๋ฉด ๊ทธ๋‚  ์š”์•ฝ์ด ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์ˆ˜๋™ ์‹คํ–‰์€ ๋ฐ˜๋“œ์‹œ ๋น ๋œจ๋ฆฌ๋Š” ๋‚ ์ด ์ƒ๊น๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`run_news_summary` ํ•จ์ˆ˜๋ฅผ ๋งค์ผ ์ž๋™ ์‹คํ–‰ํ•˜๋Š” Airflow DAG๋ฅผ ์ •์˜ํ•˜์„ธ์š”.\n\n- DAG ID: `daily_news_summary`, ์Šค์ผ€์ค„: `@daily`\n- task_id: `'summarize'`, python_callable: `run_news_summary`",
"expected_command": "with DAG('daily_news_summary', schedule='@daily', start_date=datetime(2024, 1, 1)) as dag:\n task = PythonOperator(task_id='summarize', python_callable=run_news_summary)",
"grading_conditions": ["daily_news_summary", "@daily", "PythonOperator", "task_id='summarize'", "python_callable=run_news_summary"],
"hint": "`with DAG(...) as dag:` ๋ธ”๋ก ์•ˆ์— PythonOperator๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "์ด DAG๋Š” ๋งค์ผ ์ž์ •์— `run_news_summary`๋ฅผ ์ž๋™ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. Airflow ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ๋‹ด๋‹น์ž๊ฐ€ ์—†์–ด๋„, ์ž๋Š” ์ค‘์—๋„ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "RAG ์ฑ—๋ด‡์—์„œ ๋ฐฐ์šด Airflow DAG + PythonOperator ํŒจํ„ด์ด ๋‰ด์Šค ์š”์•ฝ๊ธฐ ์ž๋™ํ™”์— ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-news-4",
"title": "[๋‰ด์Šค ์š”์•ฝ๊ธฐ 4] st.expander โ€” ๊ธด ๋ชฉ๋ก์„ ๊น”๋”ํ•˜๊ฒŒ",
"task_description": "**์ƒํ™ฉ**\n์š”์•ฝ๋œ ๊ธฐ์‚ฌ 10๊ฐœ๋ฅผ `st.write()`๋กœ ๋‚˜์—ดํ–ˆ๋”๋‹ˆ ํŽ˜์ด์ง€ ์Šคํฌ๋กค์ด ๋์ด ์—†๊ณ , ์›ํ•˜๋Š” ๊ธฐ์‚ฌ๋ฅผ ์ฐพ๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n๋ชจ๋“  ๋‚ด์šฉ์„ ํŽผ์ณ๋†“์œผ๋ฉด ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`summaries` ๋ฆฌ์ŠคํŠธ(`{'title': ..., 'content': ...}`)๋ฅผ `st.expander`๋กœ ๋ Œ๋”๋งํ•˜์„ธ์š”. ์ œ๋ชฉ๋งŒ ๋ณด์ด๊ณ  ํด๋ฆญ ์‹œ ์š”์•ฝ์ด ํŽผ์ณ์ง€๋„๋ก.",
"expected_command": "for s in summaries:\n with st.expander(s['title']):\n st.write(s['content'])",
"grading_conditions": ["st.expander", "s['title']", "st.write", "s['content']"],
"hint": "`with st.expander(์ œ๋ชฉ):` ๋ธ”๋ก ์•ˆ์— ๋‚ด์šฉ์„ ๋„ฃ์œผ๋ฉด ํด๋ฆญ ์‹œ ํŽผ์ณ์ง‘๋‹ˆ๋‹ค.",
"explanation": "`st.expander`๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ ‘ํ˜€ ์žˆ๊ณ  ํด๋ฆญํ•˜๋ฉด ๋‚ด์šฉ์ด ๋‚˜์˜ต๋‹ˆ๋‹ค. ํ•ญ๋ชฉ์ด ๋งŽ์„ ๋•Œ ํ™”๋ฉด์„ ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์ƒ์„ธ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "st.expander๋Š” ํ•ญ๋ชฉ์ด ๋งŽ์„ ๋•Œ ์ œ๋ชฉ๋งŒ ๋ณด์—ฌ์ฃผ๊ณ  ํด๋ฆญ ์‹œ ๋‚ด์šฉ์„ ํŽผ์น˜๋Š” Streamlit์˜ ํ•ต์‹ฌ UI ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค."
},
{
"id": "docs-sql-1",
"title": "[Text-to-SQL 1] SQLDatabase โ€” DB ์Šคํ‚ค๋งˆ๋ฅผ LLM์— ์•Œ๋ ค์ฃผ๊ธฐ",
"task_description": "**ํ”„๋กœ์ ํŠธ: Text-to-SQL ์ƒ์„ฑ๊ธฐ**\nํŒ€์›๋“ค์ด \"์ด ๋‹ฌ ๋งค์ถœ ์ƒ์œ„ 5๊ฐœ ์ƒํ’ˆ์ด ๋ญ์—์š”?\"๋ฅผ ๋ฌผ์–ด๋ณผ ๋•Œ๋งˆ๋‹ค SQL์„ ๋Œ€์‹  ์จ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋ฃจ์—๋„ ์ˆ˜์‹ญ ๋ฒˆ์ž…๋‹ˆ๋‹ค.\n\n---\n\n**๋‹จ๊ณ„ 1**: LLM์—๊ฒŒ DB ๊ตฌ์กฐ๋ฅผ ์•Œ๋ ค์ค˜์•ผ ์˜ฌ๋ฐ”๋ฅธ SQL์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.\n`SQLDatabase`๋กœ `./shop.db`์— ์—ฐ๊ฒฐํ•˜์„ธ์š”. (์Šคํ‚ค๋งˆ๋ฅผ ์ž๋™์œผ๋กœ ์ฝ์–ด LLM ํ”„๋กฌํ”„ํŠธ์— ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค)\n\n๊ฒฐ๊ณผ๋ฅผ `db` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "db = SQLDatabase.from_uri('sqlite:///./shop.db')",
"grading_conditions": ["SQLDatabase.from_uri", "sqlite", "shop.db"],
"hint": "`SQLDatabase.from_uri('sqlite:///๊ฒฝ๋กœ')` ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์Šฌ๋ž˜์‹œ๊ฐ€ 3๊ฐœ์ž…๋‹ˆ๋‹ค.",
"explanation": "`SQLDatabase.from_uri()`๋Š” ์—ฐ๊ฒฐ๊ณผ ๋™์‹œ์— ํ…Œ์ด๋ธ”๋ช…ยท์ปฌ๋Ÿผ ์Šคํ‚ค๋งˆ๋ฅผ ์ฝ์–ด LLM ํ”„๋กฌํ”„ํŠธ์— ์ž๋™ ์ฃผ์ž…ํ•  ์ค€๋น„๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. LLM์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ์„ ์“ฐ๋Š” ์‹ค์ˆ˜๋ฅผ ์ค„์—ฌ์ค๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "SQLDatabase.from_uri()๊ฐ€ DB ์Šคํ‚ค๋งˆ๋ฅผ ์ž๋™์œผ๋กœ ์ฝ์–ด LLM์ด ์กด์žฌํ•˜๋Š” ํ…Œ์ด๋ธ”ยท์ปฌ๋Ÿผ๋งŒ ์ฐธ์กฐํ•˜๋„๋ก ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-sql-2",
"title": "[Text-to-SQL 2] create_sql_query_chain โ€” ์ž์—ฐ์–ด โ†’ SQL",
"task_description": "**์ƒํ™ฉ**\nLLM์—๊ฒŒ ์ง์ ‘ \"SQL ์จ์ค˜\"๋ผ๊ณ  ํ–ˆ๋”๋‹ˆ:\n```sql\nSELECT * FROM sales ORDER BY revenue DESC LIMIT 5\n```\n`sales` ํ…Œ์ด๋ธ”์ด ์—†๊ณ  `orders`๊ฐ€ ๋งž๋Š”๋ฐ, LLM์ด DB ์Šคํ‚ค๋งˆ๋ฅผ ๋ชฐ๋ผ์„œ ํ‹€๋ฆฐ ํ…Œ์ด๋ธ”๋ช…์„ ์ผ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nLLM์ด ์‹ค์ œ DB ๊ตฌ์กฐ๋ฅผ ๋ชจ๋ฅด๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ์„ ์”๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`create_sql_query_chain`์€ `db` ์Šคํ‚ค๋งˆ๋ฅผ ์ž๋™ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.\n`'๊ฐ€์žฅ ๋งŽ์ด ํŒ”๋ฆฐ ์ƒํ’ˆ 5๊ฐœ๋Š”?'`์œผ๋กœ ์‹คํ–‰ํ•ด `sql` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "chain = create_sql_query_chain(llm, db)\nsql = chain.invoke({'question': '๊ฐ€์žฅ ๋งŽ์ด ํŒ”๋ฆฐ ์ƒํ’ˆ 5๊ฐœ๋Š”?'})",
"grading_conditions": ["create_sql_query_chain", "llm", "db", "chain.invoke", "question"],
"hint": "`create_sql_query_chain(llm, db)`๋กœ ์ฒด์ธ ์ƒ์„ฑ ํ›„, `{'question': '...'}` ๋”•์…”๋„ˆ๋ฆฌ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "chain ๋‚ด๋ถ€์ ์œผ๋กœ DB์˜ ํ…Œ์ด๋ธ”ยท์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ํ”„๋กฌํ”„ํŠธ์— ํฌํ•จํ•ด LLM์ด ์˜ฌ๋ฐ”๋ฅธ ์Šคํ‚ค๋งˆ๋งŒ ์ฐธ์กฐํ•˜๋„๋ก ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "create_sql_query_chain์€ SQLDatabase์—์„œ ์Šคํ‚ค๋งˆ๋ฅผ ์ž๋™ ์ฃผ์ž…ํ•ด LLM์ด ์˜ฌ๋ฐ”๋ฅธ SQL์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-sql-3",
"title": "[Text-to-SQL 3] ValidSQL Guard โ€” ์ž˜๋ชป๋œ SQL ์‹คํ–‰ ์ „ ์ฐจ๋‹จ",
"task_description": "**์ƒํ™ฉ**\nLLM์ด ์ƒ์„ฑํ•œ SQL์„ ๋ฐ”๋กœ `db.run()`์— ๋„˜๊ฒผ๋”๋‹ˆ:\n```\nOperationalError: no such column: product_name\n```\n๋ฌธ๋ฒ• ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” SQL์ด ์‹ค์ œ DB์— ์‹คํ–‰๋์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nLLM์€ ๊ฐ€๋” ๋ฌธ๋ฒ• ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” SQL์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ ์ „์— ๊ฑธ๋Ÿฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nGuardrails์˜ `ValidSQL`๋กœ SQL ์œ ํšจ์„ฑ ๊ฒ€์ฆ Guard๋ฅผ ๋งŒ๋“œ์„ธ์š”.\n- ์‹คํŒจ ์‹œ: `'reask'` (์˜ฌ๋ฐ”๋ฅธ SQL์„ ๋‹ค์‹œ ์š”์ฒญ)",
"expected_command": "guard = Guard().use(ValidSQL(on_fail='reask'))",
"grading_conditions": ["Guard()", "ValidSQL", "on_fail='reask'"],
"hint": "`Guard().use(ValidSQL(on_fail='...'))` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`on_fail='reask'`๋Š” SQL ๊ฒ€์ฆ ์‹คํŒจ ์‹œ LLM์—๊ฒŒ ์ž๋™์œผ๋กœ ์žฌ์ž‘์„ฑ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. `exception`๊ณผ ๋‹ฌ๋ฆฌ ์‚ฌ์šฉ์ž๋Š” ์˜ค๋ฅ˜๋ฅผ ๋ณด์ง€ ๋ชปํ•˜๊ณ  ์ž๋™์œผ๋กœ ์ˆ˜์ •๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "RAG ์ฑ—๋ด‡์—์„œ ๋ฐฐ์šด Guard().use() ํŒจํ„ด์ด SQL ๊ฒ€์ฆ์—๋„ ๊ทธ๋Œ€๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. on_fail='reask'๋กœ ์‚ฌ์šฉ์ž ๋…ธ์ถœ ์—†์ด ์ž๋™ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-sql-4",
"title": "[Text-to-SQL 4] FastAPI โ€” ํŒ€ ์ „์ฒด๊ฐ€ ์ž์—ฐ์–ด๋กœ DB ์กฐํšŒ",
"task_description": "**์ƒํ™ฉ**\n์Šคํฌ๋ฆฝํŠธ๋กœ ๋Œ๋ฆฌ๋ฉด ๋‚˜๋งŒ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŒ€์›๋“ค๋„ ์ž์—ฐ์–ด๋กœ DB๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๊ฒŒ API๋กœ ๋…ธ์ถœํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์Šคํฌ๋ฆฝํŠธ๋Š” ๊ณต์œ ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. API ์—”๋“œํฌ์ธํŠธ๋กœ ๋…ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n์•„๋ž˜ `???`๋ฅผ ์ฑ„์›Œ POST `/sql` ์—”๋“œํฌ์ธํŠธ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.\n\n```python\n@app.post('/sql')\nasync def generate_sql(q: Query):\n sql = ???\n result = db.run(sql)\n return {'sql': sql, 'result': result}\n```",
"expected_command": "sql = chain.invoke({'question': q.question})",
"grading_conditions": ["chain.invoke", "question", "q.question"],
"hint": "Pydantic ๋ชจ๋ธ `q`์˜ `.question` ์†์„ฑ์œผ๋กœ ์ž์—ฐ์–ด ์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.",
"explanation": "`q.question`์œผ๋กœ ์ž์—ฐ์–ด ์งˆ๋ฌธ์„ ๋ฐ›์•„ ์ฒด์ธ์— ๋„˜๊น๋‹ˆ๋‹ค. `db.run(sql)`๋กœ SQL ์‹คํ–‰ ๊ฒฐ๊ณผ๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•ด ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ”๋กœ ํ‘œ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "RAG ์ฑ—๋ด‡ FastAPI ํŒจํ„ด๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. q.question์œผ๋กœ ์งˆ๋ฌธ์„ ๋ฐ›์•„ chain.invoke()์— ๋„˜๊ธฐ๋Š” ๊ตฌ์กฐ๊ฐ€ ์žฌ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-brain-1",
"title": "[Second Brain 1] DirectoryLoader โ€” ๋…ธํŠธ 300๊ฐœ๋ฅผ ํ•œ ๋ฒˆ์—",
"task_description": "**ํ”„๋กœ์ ํŠธ: ๊ฐœ์ธ Second Brain**\n๋‚ด Obsidian ๋…ธํŠธ๊ฐ€ 300๊ฐœ๊ฐ€ ๋„˜์Šต๋‹ˆ๋‹ค. \"3๊ฐœ์›” ์ „์— ๊ณต๋ถ€ํ–ˆ๋˜ ๊ทธ ๋‚ด์šฉ์ด ์–ด๋А ํŒŒ์ผ์ด์—ˆ๋”๋ผ?\"๋ฅผ ์ฐพ์œผ๋ ค๋ฉด ํŒŒ์ผ์„ ํ•˜๋‚˜์”ฉ ์—ด์–ด๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n---\n\n**๋‹จ๊ณ„ 1**: 300๊ฐœ ํŒŒ์ผ์„ ํ•˜๋‚˜์”ฉ ๋ณต์‚ฌํ•ด RAG์— ๋„ฃ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n`DirectoryLoader`๋กœ `./notes` ํด๋” ์•ˆ์˜ ๋ชจ๋“  `.md` ํŒŒ์ผ(ํ•˜์œ„ ํด๋” ํฌํ•จ)์„ ํ•œ ๋ฒˆ์— ๋กœ๋“œํ•˜์„ธ์š”.\n\n๊ฒฐ๊ณผ๋ฅผ `docs` ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์„ธ์š”.",
"expected_command": "loader = DirectoryLoader('./notes', glob='**/*.md')\ndocs = loader.load()",
"grading_conditions": ["DirectoryLoader", "./notes", "glob='**/*.md'", "loader.load()"],
"hint": "`DirectoryLoader('ํด๋”', glob='ํŒจํ„ด')`์œผ๋กœ ๋กœ๋”๋ฅผ ๋งŒ๋“ค๊ณ  `.load()`๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"explanation": "`**/*.md`๋Š” `./notes`์™€ ๋ชจ๋“  ํ•˜์œ„ ํด๋”์—์„œ `.md` ํŒŒ์ผ์„ ์žฌ๊ท€ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. 300๊ฐœ ํŒŒ์ผ์ด ํ•œ ๋ฒˆ์˜ `loader.load()` ํ˜ธ์ถœ๋กœ ๋ชจ๋‘ `Document` ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "RAG ์ฑ—๋ด‡์—์„œ ๋ฐฐ์šด DirectoryLoader + glob ํŒจํ„ด์ด Second Brain ๋…ธํŠธ ๋กœ๋“œ์— ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-brain-2",
"title": "[Second Brain 2] metadata['source'] โ€” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์–ด๋А ํŒŒ์ผ์ธ์ง€",
"task_description": "**์ƒํ™ฉ**\nRAG ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™”๋Š”๋ฐ \"์ด๊ฒŒ ์–ด๋А ๋…ธํŠธ์—์„œ ์™”์ง€?\" ์›๋ณธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‚ด์šฉ๋งŒ ๋ณด์ด๊ณ  ์ถœ์ฒ˜๊ฐ€ ์—†์œผ๋‹ˆ ์›๋ณธ์„ ์—ด์–ด ๋งฅ๋ฝ์„ ๋” ๋ณผ ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nDocument ๊ฐ์ฒด์— ์ถœ์ฒ˜ ์ •๋ณด๊ฐ€ ์žˆ๋Š”๋ฐ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\n`docs` ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ๋ฌธ์„œ์˜ ํŒŒ์ผ ๊ฒฝ๋กœ(์ถœ์ฒ˜)์™€ ๋‚ด์šฉ ๊ธธ์ด๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.",
"expected_command": "for doc in docs:\n print(doc.metadata['source'], len(doc.page_content))",
"grading_conditions": ["doc.metadata['source']", "len(doc.page_content)"],
"hint": "ํŒŒ์ผ ๊ฒฝ๋กœ๋Š” `doc.metadata['source']`, ๋‚ด์šฉ์€ `doc.page_content`์— ์žˆ์Šต๋‹ˆ๋‹ค.",
"explanation": "`doc.metadata['source']`์— ํŒŒ์ผ ๊ฒฝ๋กœ๊ฐ€ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ ์ด ๊ฐ’์„ ํ•จ๊ป˜ ํ‘œ์‹œํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์›๋ณธ ํŒŒ์ผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "doc.metadata['source']์— ์›๋ณธ ํŒŒ์ผ ๊ฒฝ๋กœ๊ฐ€ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. RAG ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ์ถœ์ฒ˜๋ฅผ ํ‘œ์‹œํ•  ๋•Œ ํ•ญ์ƒ ์ด ๊ฐ’์„ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-brain-3",
"title": "[Second Brain 3] FileSensor โ€” ์ƒˆ ๋…ธํŠธ ์ถ”๊ฐ€ ์‹œ ์ž๋™ ์žฌ์ธ๋ฑ์‹ฑ",
"task_description": "**์ƒํ™ฉ**\n์ƒˆ ๋…ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋Š”๋ฐ ๊ฒ€์ƒ‰์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. `python reindex.py`๋ฅผ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋…ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์žฌ์ธ๋ฑ์‹ฑ์„ ์žŠ์–ด๋ฒ„๋ฆฌ๋Š” ๋‚ ์ด ์ƒ๊น๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\n์ƒˆ ํŒŒ์ผ ์ถ”๊ฐ€๋ฅผ ๊ฐ์ง€ํ•ด ์ž๋™์œผ๋กœ ์žฌ์ธ๋ฑ์‹ฑ์„ ํŠธ๋ฆฌ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nAirflow `FileSensor`๋กœ `./notes` ํด๋”์— ์ƒˆ `.md` ํŒŒ์ผ์ด ์ƒ๊ธฐ๋ฉด ๊ฐ์ง€ํ•˜๋Š” ํƒœ์Šคํฌ๋ฅผ ๋งŒ๋“œ์„ธ์š”.\n- task_id: `'wait_for_new_note'`\n- filepath: `'./notes/*.md'`\n- poke_interval: `300` (5๋ถ„๋งˆ๋‹ค ํ™•์ธ)",
"expected_command": "wait_for_new_note = FileSensor(\n task_id='wait_for_new_note',\n filepath='./notes/*.md',\n poke_interval=300\n)",
"grading_conditions": ["FileSensor", "task_id='wait_for_new_note'", "filepath", "poke_interval=300"],
"hint": "`FileSensor(task_id='...', filepath='...', poke_interval=์ดˆ)` ํŒจํ„ด์ž…๋‹ˆ๋‹ค.",
"explanation": "`FileSensor`๋Š” ์ง€์ •ํ•œ ํŒŒ์ผ์ด ์กด์žฌํ•  ๋•Œ๊นŒ์ง€ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์ด ๊ฐ์ง€๋˜๋ฉด ๋‹ค์Œ ํƒœ์Šคํฌ(์žฌ์ธ๋ฑ์‹ฑ)๊ฐ€ ์ž๋™ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. `poke_interval=300`์€ 5๋ถ„๋งˆ๋‹ค ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.",
"difficulty": "medium",
"takeaway": "FileSensor๋Š” ํŒŒ์ผ ์ถ”๊ฐ€๋ฅผ ๊ฐ์ง€ํ•ด ํ›„์† Airflow ํƒœ์Šคํฌ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. @daily์™€ ๋‹ฌ๋ฆฌ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค."
},
{
"id": "docs-brain-4",
"title": "[Second Brain 4] Streamlit โ€” ๋…ธํŠธ ๊ฒ€์ƒ‰ UI",
"task_description": "**์ƒํ™ฉ**\n์ง€๊ธˆ์€ ํ„ฐ๋ฏธ๋„์—์„œ `retriever.invoke('๊ฒ€์ƒ‰์–ด')`๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŒ€์›๋“ค์ด ์“ธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n\n**๋ฌธ์ œ**\nCLI๋Š” ๊ฐœ๋ฐœ์ž๋งŒ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n**ํ•ด๊ฒฐ**\nStreamlit์œผ๋กœ ๊ฒ€์ƒ‰ UI๋ฅผ ๋งŒ๋“œ์„ธ์š”.\n- ํด๋” ๊ฒฝ๋กœ ํ…์ŠคํŠธ ์ž…๋ ฅ (`folder`, ๊ธฐ๋ณธ๊ฐ’ `'./notes'`)\n- ๊ฒ€์ƒ‰์–ด ํ…์ŠคํŠธ ์ž…๋ ฅ (`query`)\n- '๊ฒ€์ƒ‰' ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ `retriever.invoke(query)` ์‹คํ–‰ โ†’ `results`์— ์ €์žฅ",
"expected_command": "folder = st.text_input('๋…ธํŠธ ํด๋” ๊ฒฝ๋กœ', value='./notes')\nquery = st.text_input('๊ฒ€์ƒ‰ํ•  ๋‚ด์šฉ')\nif st.button('๊ฒ€์ƒ‰'):\n results = retriever.invoke(query)",
"grading_conditions": ["st.text_input", "st.button('๊ฒ€์ƒ‰')", "retriever.invoke(query)"],
"hint": "`st.text_input`, `st.button`, `retriever.invoke` ์„ธ ๊ฐ€์ง€๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์”๋‹ˆ๋‹ค.",
"explanation": "`if st.button('๊ฒ€์ƒ‰'):` ๋ธ”๋ก ์•ˆ์— ๋ฌด๊ฑฐ์šด ์ž‘์—…์„ ๋„ฃ์œผ๋ฉด ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํด๋” ๊ฒฝ๋กœ๊ฐ€ ๋ฐ”๋€Œ๋ฉด `st.cache_resource`๋ฅผ clearํ•ด retriever๋ฅผ ์žฌ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์™„์„ฑ์ž…๋‹ˆ๋‹ค.",
"difficulty": "easy",
"takeaway": "st.text_input + st.button + retriever.invoke ์กฐํ•ฉ์€ RAG ์ฑ—๋ด‡์—์„œ ๋ฐฐ์šด Streamlit ํŒจํ„ด์ด Second Brain UI์— ๊ทธ๋Œ€๋กœ ์ ์šฉ๋œ ์˜ˆ์ž…๋‹ˆ๋‹ค."
}
]