diff --git a/Last3Mon/64/LAB-01-FIRST-WORKFLOW.md b/Last3Mon/64/LAB-01-FIRST-WORKFLOW.md index 9217fd4..e301dd9 100644 --- a/Last3Mon/64/LAB-01-FIRST-WORKFLOW.md +++ b/Last3Mon/64/LAB-01-FIRST-WORKFLOW.md @@ -6,12 +6,12 @@ ## 1. 설치 확인 (5분) -[사전 준비 안내](SETUP-WSL-OLLAMA.md)를 마친 상태에서 WSL 터미널에서 실행합니다. +[사전 준비 안내](SETUP-WSL-LLM.md)를 마친 상태에서 WSL 터미널에서 실행합니다. ```bash cd ~/<수업 저장소>/Last3Mon/64/workflow-lab -uv run pytest -q # 12 passed, 1 skipped -uv run miniflow check # Reply (ollama, ...) 가 나오면 준비 완료 +uv run pytest -q # 18 passed, 1 skipped +uv run miniflow check # Reply (openai, ...) 가 나오면 준비 완료 ``` ## 2. 가장 단순한 워크플로우 실행 (10분) @@ -33,14 +33,22 @@ uv run miniflow run flows/01-llm-only.json -i text="다음 주 화요일 10시 Workflow: 01 LLM only ------------------------------------------------------------------------ [1/3] start input ok 0.0ms {"text": "다음 주 화요일 ..."} -[2/3] summarize llm ok 7071.5ms "다음 주 화요일 10시에 기획 회의가 진행됩니다. 각 팀은 ..." -[3/3] result output ok 0.1ms "다음 주 화요일 10시에 기획 회의가 진행됩니다. 각 팀은 ..." +[2/3] summarize llm ok 1645.8ms "다음 주 화요일 10시에 기획 회의가 있습니다. 각 팀은 진행 현황을 준비해 주세요." +[3/3] result output ok 0.0ms "다음 주 화요일 10시에 기획 회의가 있습니다. 각 팀은 진행 현황을 준비해 주세요." ------------------------------------------------------------------------ Status: completed Trace saved: runs/20261014-101500-01-llm-only.json ``` -노트북의 Ollama 모델(`gemma3:4b`)이 요약을 만들었습니다. 요약 문장과 소요 시간은 실행할 때마다, 노트북마다 다릅니다. +OpenAI의 `gpt-4.1-mini`가 요약을 만들었습니다(약 1.4~1.6초). 요약 문장과 소요 시간은 실행할 때마다 조금씩 다를 수 있습니다. + +같은 명령을 **대체 모델(Ollama)** 로도 실행해 봅니다. `.env`보다 명령 앞에 적은 값이 우선합니다. + +```bash +MINIFLOW_LLM=ollama uv run miniflow run flows/01-llm-only.json -i text="다음 주 화요일 10시에 기획 회의를 진행합니다. 각 팀은 진행 현황을 준비해 주세요." +``` + +수업 전 측정에서 `gemma3:4b`는 12.7초가 걸렸고, 원문에 없는 "회의에서 진행 상황을 공유하고 논의할 예정입니다"를 덧붙였습니다. 요약 노드도 **내용을 지어낼 수 있습니다**. 같은 명령을 **Mock LLM**으로도 실행해 비교해 봅니다. Mock은 노드에 적어 둔 정해진 답을 돌려줍니다. @@ -54,7 +62,7 @@ MINIFLOW_LLM=mock uv run miniflow run flows/01-llm-only.json -i text="다음 주 1. `summarize` 노드의 `prompt` 필드에는 무엇이 들어 있나요? `{{input.text}}`는 어떻게 바뀌었나요? 2. `provider` 값은 무엇인가요? Mock으로 실행한 기록과 비교하면 무엇이 다른가요? -3. 각 노드의 `ms` 값을 보면 어느 노드가 가장 오래 걸렸나요? Mock으로 실행했을 때와 비교하면 얼마나 차이 나나요? +3. 각 노드의 `ms` 값을 보면 어느 노드가 가장 오래 걸렸나요? Mock, Ollama로 실행했을 때와 비교하면 얼마나 차이 나나요? ```bash uv run miniflow show flows/01-llm-only.json diff --git a/Last3Mon/64/LAB-02-LLM-TOOL-MEMORY.md b/Last3Mon/64/LAB-02-LLM-TOOL-MEMORY.md index 0bb615d..5111c02 100644 --- a/Last3Mon/64/LAB-02-LLM-TOOL-MEMORY.md +++ b/Last3Mon/64/LAB-02-LLM-TOOL-MEMORY.md @@ -13,7 +13,7 @@ uv run miniflow run flows/02-llm-tool.json -i city=제주 -i question="우산이 ```text [2/4] weather tool ok 0.1ms {"city": "제주", "condition": "비", "temp_c": 20, "rain_chance": 80, ...} -[3/4] answer llm ok 3783.8ms "네, 비가 오므로 우산이 필요할 것 같습니다." +[3/4] answer llm ok 1426.2ms "네, 제주에 비가 오고 비 올 확률이 80%이므로 우산이 필요합니다." ``` 확인할 것 @@ -71,36 +71,37 @@ uv run miniflow run flows/my-02-with-count.json -i city=서울 -i question="산 `validate`가 오류를 내면 오류 메시지를 읽고 고칩니다. 대부분 노드 id 오타나 연결 누락입니다. -## 4. Mock과 실제 LLM 비교 (10분) +## 4. Mock·OpenAI·대체 모델 비교 (10분) -지금까지는 Ollama 모델이 답했습니다. 같은 워크플로우를 Mock과 번갈아 실행해 비교합니다. +지금까지는 OpenAI `gpt-4.1-mini`가 답했습니다. 같은 워크플로우를 Mock, 대체 모델(Ollama)과 번갈아 실행해 비교합니다. ```bash uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" MINIFLOW_LLM=mock uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" +MINIFLOW_LLM=ollama uv run miniflow run flows/05-tech-support-ticket.json -i user=kim -i ticket="프린터에서 출력이 안 됩니다" ``` -| 비교할 것 | Mock | Ollama | -| --- | --- | --- | -| `classify`·`draft` 노드의 `ms` | | | -| 두 번 실행한 답변이 같은가? | | | -| 분류 결과가 정해진 값(`device`)과 정확히 같은가? | | | +| 비교할 것 | Mock | OpenAI | Ollama | +| --- | --- | --- | --- | +| `classify`·`draft` 노드의 `ms` | | | | +| 두 번 실행한 답변이 같은가? | | | | +| 분류 결과가 정해진 값(`device`)과 정확히 같은가? | | | | -수업 전에 확인한 결과(CPU 1개, 메모리 8GB 환경)는 다음과 같았습니다. 내 노트북 결과와 비교해 보세요. +수업 전에 같은 문의 4건(비밀번호, VPN, 프린터, 메일 프로그램 설치)으로 확인한 결과입니다. 내 노트북 결과와 비교해 보세요. Ollama 시간은 노트북 성능에 따라 크게 다릅니다. | 모델 | 분류 4건 정답 | 분류 노드 | 답변 초안 노드 | | --- | --- | --- | --- | -| `gemma3:1b` | 0/4 (전부 `software`, 한 번은 `Software`) | 약 1초 | 4~7초 | -| `gemma3:4b` | 4/4 | 약 1.7초 | 7~11초 | +| `gpt-4.1-mini` (OpenAI, 기본) | 4/4 | 약 1~2.6초 | 약 1.2~1.7초 | +| `gemma3:4b` (대체) | 4/4 | 약 1.6~2.3초 | 7~11초 | +| `gemma3:1b` (8GB 노트북 대체) | 1/4 (나머지 `software`, 대문자 `Software`가 나온 적도 있음) | 약 1초 | 4~7초 | -작은 모델은 빠르지만 판단이 틀리거나 대소문자처럼 형식이 흔들립니다. 다음 노드가 분류값을 비교해야 한다면, LLM 출력을 그대로 믿지 말고 **검증하는 단계**가 필요합니다. (73일차 입력·출력 검증) +작은 모델은 빠르지만 판단이 틀리거나 대소문자처럼 형식이 흔들립니다. 8GB 노트북은 `OLLAMA_MODEL=gemma3:1b MINIFLOW_LLM=ollama`로, 메모리가 넉넉하면 두 모델을 모두 돌려 봅니다. 다음 노드가 분류값을 비교해야 한다면, LLM 출력을 그대로 믿지 말고 **검증하는 단계**가 필요합니다. (73일차 입력·출력 검증) -(선택) OpenAI/Anthropic/Gemini 중 API 키가 있으면 클라우드 모델과도 비교할 수 있습니다. 가지고 있는 키 하나만 설정하면 됩니다. +(선택) Anthropic·Gemini 키가 있으면 다른 제공사 모델과도 비교할 수 있습니다. 가지고 있는 키 하나만 설정하면 됩니다. | 제공사 | 설치 | 설정 | | --- | --- | --- | -| OpenAI | `uv sync --extra openai` | `export MINIFLOW_LLM=openai`, `export OPENAI_API_KEY=...` | | Anthropic | `uv sync --extra anthropic` | `export MINIFLOW_LLM=anthropic`, `export ANTHROPIC_API_KEY=...` | | Gemini (10일차에 발급한 키) | `uv sync --extra gemini` | `export MINIFLOW_LLM=gemini`, `export GEMINI_API_KEY=...` | @@ -117,4 +118,4 @@ MINIFLOW_LLM=mock uv run miniflow run flows/05-tech-support-ticket.json -i user= - [ ] Tool 결과가 LLM 프롬프트에 들어간 것을 실행 기록에서 찾았다. - [ ] Memory가 두 번의 실행을 이어 주는 것을 확인했다. - [ ] `my-02-with-count.json`을 만들어 `validate`와 `run`을 통과했다. -- [ ] Mock과 Ollama의 결과·속도 차이를 표로 정리했다. +- [ ] Mock·OpenAI·Ollama의 결과·속도 차이를 표로 정리했다. diff --git a/Last3Mon/64/LAB-03-SCENARIO-ANALYSIS.md b/Last3Mon/64/LAB-03-SCENARIO-ANALYSIS.md index b8883b7..6662784 100644 --- a/Last3Mon/64/LAB-03-SCENARIO-ANALYSIS.md +++ b/Last3Mon/64/LAB-03-SCENARIO-ANALYSIS.md @@ -23,7 +23,14 @@ uv run miniflow run flows/05-tech-support-ticket.json -i user=lee -i ticket="점 | 2번째 | `history` 노드 출력에 1번째 문의가 남아 있는가? | | 3번째 | 어느 노드에서 왜 멈추었는가? `draft` 노드는 무엇이라고 답했는가? 이 경우 실제 업무에서는 어떻게 처리해야 할까? | -3번째 실행에서는 FAQ 검색 결과가 없는데도 LLM이 그럴듯한 답변 초안을 만들고, 마지막 `result` 노드에서 멈춥니다. Mock으로 실행(`MINIFLOW_LLM=mock`)하면 `draft` 노드에서 바로 멈춥니다. **근거가 없을 때 실제 LLM이 어떻게 행동하는지** 실행 기록에서 확인해 보세요. +3번째 실행에서는 FAQ 검색 결과가 없는데도 LLM이 답변 초안을 만들고, 마지막 `result` 노드에서 멈춥니다. Mock으로 실행(`MINIFLOW_LLM=mock`)하면 `draft` 노드에서 바로 멈춥니다. **근거가 없을 때 실제 LLM이 어떻게 행동하는지** 실행 기록에서 확인해 보세요. + +| 모델 (수업 전 측정) | `draft`가 쓴 답 | `result` | +| --- | --- | --- | +| `gpt-4.1-mini` | "점심 메뉴 추천에 관한 FAQ가 현재 제공되지 않아 … 양해 부탁드립니다" (솔직하게 답함) | `'0' not found`로 실패 | +| 대체 `gemma3:4b` (`MINIFLOW_LLM=ollama`) | "… FAQ 검색 결과가 없습니다. (FAQ ID: other-123)" (**없는 FAQ 번호를 지어냄**) | `'0' not found`로 실패 | + +좋은 모델은 근거가 없다고 말했지만, 워크플로우는 어느 쪽이든 마지막에 가서야 멈춥니다. 모델이 바뀌어도 안전하려면 "FAQ가 없으면 담당자에게" 같은 **경로**가 필요합니다 (65일차 Condition). `uv run miniflow show flows/05-tech-support-ticket.json`으로 구조도 확인합니다. @@ -82,7 +89,7 @@ uv run miniflow run flows/my-scenario.json -i user=me -i text="..." Tip -- LLM 노드의 프롬프트에는 답의 형식(정해진 값, 문장 수 등)을 분명히 적습니다. Ollama 답이 매번 달라 흐름 확인이 어려우면 `mock`의 `rules`·`template`을 함께 적고 `MINIFLOW_LLM=mock`으로 구조부터 확인합니다. +- LLM 노드의 프롬프트에는 답의 형식(정해진 값, 문장 수 등)을 분명히 적습니다. LLM 답이 매번 달라 흐름 확인이 어려우면 `mock`의 `rules`·`template`을 함께 적고 `MINIFLOW_LLM=mock`으로 구조부터 확인합니다. - 사용할 수 있는 Tool은 `uv run miniflow tools`로 확인합니다 (`get_weather`, `search_faq`, `count_chars`). - `show` 결과를 분석 문서의 "구현한 워크플로우" 항목에 붙여 넣습니다. diff --git a/Last3Mon/64/README.md b/Last3Mon/64/README.md index ceddf6c..0a59fc9 100644 --- a/Last3Mon/64/README.md +++ b/Last3Mon/64/README.md @@ -10,7 +10,7 @@ ```text Last3Mon/64/ ├── README.md -├── SETUP-WSL-OLLAMA.md # 사전 준비 · WSL과 Ollama 로컬 LLM +├── SETUP-WSL-LLM.md # 사전 준비 · WSL, OpenAI API 키(.env), 대체 Ollama ├── LAB-01-FIRST-WORKFLOW.md # 1단계 · 첫 워크플로우 실행과 실행 기록 읽기 ├── LAB-02-LLM-TOOL-MEMORY.md # 2단계 · LLM·Tool·Memory 연결하기 ├── LAB-03-SCENARIO-ANALYSIS.md # 3단계 · 업무 자동화 시나리오 분석과 설계 @@ -41,11 +41,12 @@ flowchart LR ## 준비 (수업 전날) -실습은 Windows 11의 WSL(Ubuntu)에서, LLM은 노트북에서 실행하는 Ollama(`gemma3:4b`, 8GB 노트북은 `gemma3:1b`)로 진행합니다. API 키는 필요 없습니다. +실습은 Windows 11의 WSL(Ubuntu)에서 진행합니다. LLM은 OpenAI API(`gpt-4.1-mini`, 키는 `Last3Mon/.env`)를 쓰고, 키·인터넷에 문제가 있으면 노트북의 Ollama(`gemma3:4b`, 8GB 노트북은 `gemma3:1b`)가 대신 답합니다. -먼저 **[실습 준비 안내](../../Lecture/64/00-DAY64-PREP.md)** 로 전체 준비 항목을 확인하고, 설치는 **[SETUP-WSL-OLLAMA.md](SETUP-WSL-OLLAMA.md)** 의 순서대로 진행한 뒤 마지막에 아래 두 명령이 통과하는지 확인합니다. +먼저 **[실습 준비 안내](../../Lecture/64/00-DAY64-PREP.md)** 로 전체 준비 항목을 확인하고, 설치는 **[SETUP-WSL-LLM.md](SETUP-WSL-LLM.md)** 의 순서대로 진행한 뒤 마지막에 아래 명령이 통과하는지 확인합니다. ```bash -uv run pytest -q # 12 passed, 1 skipped -uv run miniflow check # Reply (ollama, ...) +uv run pytest -q # 18 passed, 1 skipped +uv run miniflow check # Reply (openai, ...) +OPENAI_API_KEY=sk-wrong uv run miniflow check # Reply (ollama, ...) : 대체 경로 ``` diff --git a/Last3Mon/64/SETUP-WSL-LLM.md b/Last3Mon/64/SETUP-WSL-LLM.md new file mode 100644 index 0000000..5497c94 --- /dev/null +++ b/Last3Mon/64/SETUP-WSL-LLM.md @@ -0,0 +1,181 @@ +# 사전 준비 · WSL 실습 환경과 LLM (OpenAI API 기본, Ollama 대체) + +64일차 실습은 Windows 11의 **WSL(Ubuntu)** 에서 진행합니다. LLM은 **OpenAI API**(기본 모델 `gpt-4.1-mini`)를 쓰고, 키가 없거나 인터넷·API에 문제가 있으면 노트북에서 직접 실행하는 **Ollama**(`gemma3:4b`)가 대신 답합니다. 이 구성은 65일차 이후 모든 실습에서 그대로 씁니다. + +> Ollama 모델 파일이 1~3GB입니다. 수업 중에 21명이 동시에 받으면 강의실 네트워크가 느려지므로 **수업 전날 5단계(모델 다운로드)까지 끝내 주세요.** + +```mermaid +flowchart LR + subgraph WIN["Windows 11"] + T["Windows Terminal"] + subgraph WSL["WSL2 · Ubuntu"] + E[".env
OPENAI_API_KEY
(git 제외)"] -.-> M + M["miniflow
(uv 프로젝트)"] -.->|"대체
localhost:11434"| O["Ollama 서버"] + O --> G[("gemma3:4b 모델")] + end + T --> WSL + end + M -->|"기본"| AI["OpenAI API
gpt-4.1-mini"] +``` + +## 1. WSL 확인 (5분) + +PowerShell에서 확인합니다. + +```powershell +wsl --list --verbose +``` + +`Ubuntu`가 `VERSION 2`로 보이면 됩니다. 없으면 `wsl --install -d Ubuntu`로 설치하고 노트북을 다시 시작합니다. + +이후 명령은 모두 **WSL(Ubuntu) 터미널**에서 실행합니다. + +## 2. 실습 코드와 uv 준비 (10분) + +```bash +# uv (이미 설치되어 있으면 건너뜀) +curl -LsSf https://astral.sh/uv/install.sh | sh +source ~/.bashrc + +# 실습 폴더는 WSL 홈(~) 아래에 둡니다. /mnt/c 아래는 느리고 권한 문제가 생깁니다. +cd ~ +# 수업 자료를 받는 방법은 강사가 따로 안내합니다. +cd ~/<수업 저장소>/Last3Mon/64/workflow-lab +uv sync +uv run pytest -q +``` + +`18 passed, 1 skipped`가 나오면 됩니다. (`skipped` 1개는 선택 기능인 Anthropic 연결 테스트로, 해당 패키지를 설치하지 않으면 건너뜁니다.) 테스트는 `.env`를 읽지 않고 Mock LLM으로만 돌기 때문에 키가 없어도 통과합니다. + +## 3. OpenAI API 키를 `.env`에 넣기 (3분) + +과정에서 받은 OpenAI API 키를 `Last3Mon/.env` 한 곳에 넣습니다. `miniflow`는 실행한 폴더에서 위로 올라가며 가장 가까운 `.env`를 읽으므로, 64일차부터 모든 일차가 이 파일 하나를 씁니다. + +```bash +cd ~/<수업 저장소>/Last3Mon +cp .env.example .env +nano .env # OPENAI_API_KEY= 뒤에 키를 붙여 넣고 저장 (Ctrl+O, Enter, Ctrl+X) +``` + +| 줄 | 뜻 | +| --- | --- | +| `OPENAI_API_KEY=` | 받은 키 (`sk-`로 시작). **문서·워크플로우·채팅·화면 캡처에 올리지 않습니다** | +| `OPENAI_MODEL=gpt-4.1-mini` | 수업 기본 모델 | +| `MINIFLOW_LLM=openai` | `miniflow`가 OpenAI를 씀. 키를 받지 못했으면 `ollama`로 바꿈 | +| `MINIFLOW_LLM_FALLBACK=ollama` | OpenAI 호출이 실패하면 Ollama로 한 번 더 시도 | +| `OLLAMA_MODEL=gemma3:4b` | 대체 모델. 8GB 노트북은 `gemma3:1b` | + +> `.env`는 `.gitignore`에 들어 있어 커밋되지 않습니다. 키가 들어간 파일을 다른 이름으로 복사해 두지 마세요. +> +> 셸에 `export MINIFLOW_LLM=...`을 적어 두었다면(`~/.bashrc`) 그 값이 `.env`보다 우선합니다. 지난 실습에서 넣은 줄이 있으면 지우고 터미널을 다시 엽니다. + +## 4. 노트북 메모리에 맞는 대체 모델 고르기 + +WSL은 기본으로 **Windows 전체 메모리의 절반**까지만 사용합니다. + +| 노트북 메모리 | WSL이 쓸 수 있는 메모리 | 대체 모델 (`OLLAMA_MODEL`) | +| --- | --- | --- | +| 8GB | 약 4GB | `gemma3:1b` | +| 16GB 이상 | 약 8GB 이상 | `gemma3:4b` (권장) | + +내 노트북 메모리는 PowerShell에서 확인합니다. + +```powershell +(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB +``` + +메모리가 부족하다는 오류가 나면 `C:\Users\<사용자>\.wslconfig` 파일을 만들어 상한을 올린 뒤 PowerShell에서 `wsl --shutdown` 하고 WSL을 다시 엽니다. + +```ini +[wsl2] +memory=10GB +swap=4GB +``` + +## 5. Ollama 설치와 모델 다운로드 (10~20분) + +OpenAI가 기본이지만 Ollama는 **꼭** 준비합니다. 키·인터넷에 문제가 있을 때 대신 답하고, 2단계 실습에서 모델 크기에 따른 차이를 직접 비교합니다. + +```bash +curl -fsSL https://ollama.com/install.sh | sh +ollama --version + +# 메모리 16GB 이상 +ollama pull gemma3:4b +# 메모리 8GB +ollama pull gemma3:1b + +ollama list +``` + +`ollama list`에 모델이 보이면 됩니다. 설치 스크립트가 Ollama 서버를 자동으로 실행합니다. `ollama list`에서 연결 오류가 나면 다른 터미널 창에서 `ollama serve`를 실행해 두세요. + +## 6. 연결 확인 (5분) + +```bash +cd ~/<수업 저장소>/Last3Mon/64/workflow-lab +uv run miniflow check +``` + +```text +MINIFLOW_LLM : openai +.env : /home/<사용자>/<수업 저장소>/Last3Mon/.env +OPENAI_MODEL : gpt-4.1-mini +API key : set +OLLAMA_HOST : http://localhost:11434 +OLLAMA_MODEL : gemma3:4b +FALLBACK : ollama +Reply (openai, 2.0s): ready +``` + +대체 경로도 한 번 확인합니다. 키를 일부러 틀리게 넣으면(이 명령에서만) Ollama가 대신 답합니다. + +```bash +OPENAI_API_KEY=sk-wrong uv run miniflow check +``` + +```text +[miniflow] openai failed (OpenAI rejected the API key (check OPENAI_API_KEY in .env)); trying ollama +Reply (ollama, 20.9s): Ready +``` + +Ollama의 첫 호출은 모델을 메모리에 올리느라 10~20초 걸립니다. 두 번째부터는 빨라집니다. + +## 7. 이미 Windows나 Docker에 Ollama가 있는 경우 + +9월 실습에서 Docker로 Ollama를 실행해 둔 경우에는 WSL에 새로 설치하지 않고 그대로 써도 됩니다. + +| 상황 | 설정 | +| --- | --- | +| Docker Desktop으로 실행한 Ollama (`11434` 포트 공개) | 컨테이너를 켜 두면 WSL에서도 `http://localhost:11434`로 연결됩니다. 6절의 대체 경로 확인으로 점검 | +| Windows용 Ollama 앱 | `.wslconfig`에 `networkingMode=mirrored`를 추가하고 `wsl --shutdown` 후 다시 열면 `localhost`로 연결됩니다 | + +```ini +[wsl2] +networkingMode=mirrored +``` + +## 8. 문제 해결 + +| 증상 | 해결 | +| --- | --- | +| `API key : MISSING` | `Last3Mon/.env`에 `OPENAI_API_KEY`가 비어 있음. 키를 넣거나, 키가 없으면 `MINIFLOW_LLM=ollama` | +| `.env : (none)` | `.env` 위치 확인 (`Last3Mon/.env`), 실습 폴더 안에서 실행했는지 확인 | +| `OpenAI rejected the API key` | 키를 잘못 붙여 넣음 (앞뒤 공백·줄바꿈 확인) | +| `HTTP 429 ... insufficient_quota` | 키의 크레딧이 없음. 강사에게 알리고 그동안 `MINIFLOW_LLM=ollama` | +| `Cannot reach the OpenAI API` | 인터넷·프록시 문제. 그동안 `MINIFLOW_LLM=ollama` | +| `MINIFLOW_LLM : ollama`로 나옴 | `~/.bashrc`의 `export MINIFLOW_LLM=ollama`가 `.env`보다 우선. 그 줄을 지우고 터미널 다시 열기 | +| `Cannot reach Ollama at http://localhost:11434` | `ollama serve` 실행, 또는 7절의 연결 설정 확인 | +| `Model 'gemma3:4b' not found on Ollama` | `ollama pull gemma3:4b` | +| Ollama 응답이 1분 넘게 걸림, WSL이 멈춤 | 메모리 부족. `gemma3:1b`로 바꾸거나 `.wslconfig`의 `memory` 상향 | +| `uv: command not found` | `source ~/.bashrc` 또는 터미널 다시 열기 | +| 한글이 깨져 보임 | Windows Terminal 사용 (기본 UTF-8) | +| OpenAI와 Ollama 모두 안 됨 | `export MINIFLOW_LLM=mock`으로 Mock LLM 사용 (결과는 정해진 답) | + +## 확인 + +- [ ] `uv run pytest -q` → `18 passed, 1 skipped` +- [ ] `Last3Mon/.env`에 키를 넣었다 (키는 이 파일에만 있다) +- [ ] `ollama list`에 모델이 보인다 +- [ ] `uv run miniflow check`에서 `Reply (openai, ...)`가 나온다 +- [ ] `OPENAI_API_KEY=sk-wrong uv run miniflow check`에서 `Reply (ollama, ...)`가 나온다 diff --git a/Last3Mon/64/SETUP-WSL-OLLAMA.md b/Last3Mon/64/SETUP-WSL-OLLAMA.md deleted file mode 100644 index fe4fc66..0000000 --- a/Last3Mon/64/SETUP-WSL-OLLAMA.md +++ /dev/null @@ -1,144 +0,0 @@ -# 사전 준비 · WSL에서 실습 환경과 Ollama 로컬 LLM 준비 - -64일차 실습은 Windows 11의 **WSL(Ubuntu)** 에서 진행하고, LLM은 노트북에서 직접 실행하는 **Ollama**를 사용합니다. API 키가 필요 없고 사용량 제한도 없습니다. - -> 모델 파일이 1~3GB입니다. 수업 중에 21명이 동시에 받으면 강의실 네트워크가 느려지므로 **수업 전날 4단계(모델 다운로드)까지 끝내 주세요.** - -```mermaid -flowchart LR - subgraph WIN["Windows 11"] - T["Windows Terminal"] - subgraph WSL["WSL2 · Ubuntu"] - M["miniflow
(uv 프로젝트)"] -->|"http://localhost:11434"| O["Ollama 서버"] - O --> G[("gemma3:4b 모델")] - end - T --> WSL - end -``` - -## 1. WSL 확인 (5분) - -PowerShell에서 확인합니다. - -```powershell -wsl --list --verbose -``` - -`Ubuntu`가 `VERSION 2`로 보이면 됩니다. 없으면 `wsl --install -d Ubuntu`로 설치하고 노트북을 다시 시작합니다. - -이후 명령은 모두 **WSL(Ubuntu) 터미널**에서 실행합니다. - -## 2. 노트북 메모리에 맞는 모델 고르기 - -WSL은 기본으로 **Windows 전체 메모리의 절반**까지만 사용합니다. - -| 노트북 메모리 | WSL이 쓸 수 있는 메모리 | 사용할 모델 | -| --- | --- | --- | -| 8GB | 약 4GB | `gemma3:1b` | -| 16GB 이상 | 약 8GB 이상 | `gemma3:4b` (권장) | - -내 노트북 메모리는 PowerShell에서 확인합니다. - -```powershell -(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB -``` - -메모리가 부족하다는 오류가 나면 `C:\Users\<사용자>\.wslconfig` 파일을 만들어 상한을 올린 뒤 PowerShell에서 `wsl --shutdown` 하고 WSL을 다시 엽니다. - -```ini -[wsl2] -memory=10GB -swap=4GB -``` - -## 3. 실습 코드와 uv 준비 (10분) - -```bash -# uv (이미 설치되어 있으면 건너뜀) -curl -LsSf https://astral.sh/uv/install.sh | sh -source ~/.bashrc - -# 실습 폴더는 WSL 홈(~) 아래에 둡니다. /mnt/c 아래는 느리고 권한 문제가 생깁니다. -cd ~ -# 수업 저장소를 받는 방법은 강의 시간에 안내합니다. -cd ~/<수업 저장소>/Last3Mon/64/workflow-lab -uv sync -uv run pytest -q -``` - -`12 passed, 1 skipped`가 나오면 됩니다. (`skipped` 1개는 선택 기능인 Anthropic 연결 테스트로, 해당 패키지를 설치하지 않으면 건너뜁니다.) - -## 4. Ollama 설치와 모델 다운로드 (10~20분) - -```bash -curl -fsSL https://ollama.com/install.sh | sh -ollama --version - -# 메모리 16GB 이상 -ollama pull gemma3:4b -# 메모리 8GB -ollama pull gemma3:1b - -ollama list -``` - -`ollama list`에 모델이 보이면 됩니다. 설치 스크립트가 Ollama 서버를 자동으로 실행합니다. `ollama list`에서 연결 오류가 나면 다른 터미널 창에서 `ollama serve`를 실행해 두세요. - -## 5. miniflow와 Ollama 연결 (5분) - -`~/.bashrc` 맨 아래에 추가합니다. 8GB 노트북은 모델 이름을 `gemma3:1b`로 바꿉니다. - -```bash -cat >> ~/.bashrc <<'EOF' -export MINIFLOW_LLM=ollama -export OLLAMA_MODEL=gemma3:4b -EOF -source ~/.bashrc -``` - -연결을 확인합니다. - -```bash -cd ~/<수업 저장소>/Last3Mon/64/workflow-lab -uv run miniflow check -``` - -```text -MINIFLOW_LLM : ollama -OLLAMA_HOST : http://localhost:11434 -OLLAMA_MODEL : gemma3:4b -Reply (ollama, 18.8s): Ready -``` - -첫 호출은 모델을 메모리에 올리느라 10~20초 걸립니다. 두 번째부터는 빨라집니다. - -## 6. 이미 Windows나 Docker에 Ollama가 있는 경우 - -9월 실습에서 Docker로 Ollama를 실행해 둔 경우에는 WSL에 새로 설치하지 않고 그대로 써도 됩니다. - -| 상황 | 설정 | -| --- | --- | -| Docker Desktop으로 실행한 Ollama (`11434` 포트 공개) | 컨테이너를 켜 두면 WSL에서도 `http://localhost:11434`로 연결됩니다. `uv run miniflow check`로 확인 | -| Windows용 Ollama 앱 | `.wslconfig`에 `networkingMode=mirrored`를 추가하고 `wsl --shutdown` 후 다시 열면 `localhost`로 연결됩니다 | - -```ini -[wsl2] -networkingMode=mirrored -``` - -## 7. 문제 해결 - -| 증상 | 해결 | -| --- | --- | -| `Cannot reach Ollama at http://localhost:11434` | `ollama serve` 실행, 또는 6절의 연결 설정 확인 | -| `Model 'gemma3:4b' not found on Ollama` | `ollama pull gemma3:4b` | -| 응답이 1분 넘게 걸림, WSL이 멈춤 | 메모리 부족. `gemma3:1b`로 바꾸거나 `.wslconfig`의 `memory` 상향 | -| `uv: command not found` | `source ~/.bashrc` 또는 터미널 다시 열기 | -| 한글이 깨져 보임 | Windows Terminal 사용 (기본 UTF-8) | -| Ollama가 없거나 문제가 계속됨 | `export MINIFLOW_LLM=mock`으로 Mock LLM 사용 (결과는 정해진 답) | - -## 확인 - -- [ ] `uv run pytest -q` → `12 passed, 1 skipped` -- [ ] `ollama list`에 모델이 보인다 -- [ ] `uv run miniflow check`에서 `Reply (ollama, ...)`가 나온다 diff --git a/Last3Mon/64/workflow-lab/README.md b/Last3Mon/64/workflow-lab/README.md index 761547a..c39c32f 100644 --- a/Last3Mon/64/workflow-lab/README.md +++ b/Last3Mon/64/workflow-lab/README.md @@ -5,7 +5,7 @@ ```mermaid flowchart LR J["flows/*.json
노드와 연결 정의"] --> E["miniflow 엔진
순서 계산 · 실행"] - E --> L{{"LLM 노드
ollama · mock · openai · anthropic · gemini"}} + E --> L{{"LLM 노드
openai · ollama · mock · anthropic · gemini"}} E --> T["Tool 노드
get_weather · search_faq"] E --> M[("Memory 노드
.miniflow-memory.json")] E --> R["runs/*.json
실행 기록(Trace)"] @@ -57,19 +57,18 @@ uv run pytest -q ## 5. LLM 선택 -`MINIFLOW_LLM` 환경 변수로 LLM을 고릅니다. 수업 기본값은 노트북에서 실행하는 **Ollama**입니다. 준비 방법은 [SETUP-WSL-OLLAMA.md](../SETUP-WSL-OLLAMA.md)를 참고하세요. +`MINIFLOW_LLM`으로 LLM을 고릅니다. 보통은 `Last3Mon/.env`에 적어 두고(실행한 폴더에서 위로 올라가며 가장 가까운 `.env`를 읽음), 셸이나 명령 앞에 적은 값이 `.env`보다 우선합니다. 수업 기본값은 **OpenAI API**이고, `MINIFLOW_LLM_FALLBACK=ollama`이면 OpenAI 호출이 실패할 때 노트북의 Ollama로 한 번 더 시도합니다. 준비 방법은 [SETUP-WSL-LLM.md](../SETUP-WSL-LLM.md)를 참고하세요. | `MINIFLOW_LLM` | LLM | 필요한 것 | | --- | --- | --- | -| `ollama` (수업 기본) | 로컬 모델 (`OLLAMA_MODEL`, 기본 `gemma3:4b`) | Ollama 설치, 모델 다운로드 | -| `mock` (설정하지 않았을 때) | 노드의 `mock` 설정대로 정해진 답 | 없음 (자동 테스트, Ollama가 안 될 때) | -| `openai` | OpenAI (`OPENAI_MODEL`) | `uv sync --extra openai`, `OPENAI_API_KEY` | +| `openai` (수업 기본) | OpenAI (`OPENAI_MODEL`, 기본 `gpt-4.1-mini`) | `.env`의 `OPENAI_API_KEY` (패키지는 `uv sync`에 포함) | +| `ollama` (대체) | 로컬 모델 (`OLLAMA_MODEL`, 기본 `gemma3:4b`) | Ollama 설치, 모델 다운로드 | +| `mock` (`.env`도 설정도 없을 때) | 노드의 `mock` 설정대로 정해진 답 | 없음 (자동 테스트, 둘 다 안 될 때) | | `anthropic` | Anthropic Claude (`ANTHROPIC_MODEL`, 기본 `claude-opus-5-5`) | `uv sync --extra anthropic`, `ANTHROPIC_API_KEY` | | `gemini` | Google Gemini (`GEMINI_MODEL`) | `uv sync --extra gemini`, `GEMINI_API_KEY` | ```bash -export MINIFLOW_LLM=ollama -export OLLAMA_MODEL=gemma3:4b # 8GB 노트북은 gemma3:1b -export OLLAMA_HOST=http://localhost:11434 # 기본값이라 생략 가능 -uv run miniflow check # 연결 확인 +uv run miniflow check # .env 설정으로 연결 확인 → Reply (openai, ...) +OPENAI_API_KEY=sk-wrong uv run miniflow check # 대체 경로 확인 → Reply (ollama, ...) +MINIFLOW_LLM=ollama OLLAMA_MODEL=gemma3:1b uv run miniflow check # 대체 모델만 따로 ``` diff --git a/Last3Mon/64/workflow-lab/pyproject.toml b/Last3Mon/64/workflow-lab/pyproject.toml index 80652eb..da7e3f6 100644 --- a/Last3Mon/64/workflow-lab/pyproject.toml +++ b/Last3Mon/64/workflow-lab/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "A tiny JSON-defined AI workflow runner for learning LLM, Tool and Memory nodes" readme = "README.md" requires-python = ">=3.11" -dependencies = [] +dependencies = ["openai>=1.40"] # course default LLM (key in .env) [project.optional-dependencies] gemini = ["google-genai>=1.0"] diff --git a/Last3Mon/64/workflow-lab/src/miniflow/cli.py b/Last3Mon/64/workflow-lab/src/miniflow/cli.py index e3c9e20..2be006c 100644 --- a/Last3Mon/64/workflow-lab/src/miniflow/cli.py +++ b/Last3Mon/64/workflow-lab/src/miniflow/cli.py @@ -67,9 +67,15 @@ def cmd_check(_: argparse.Namespace) -> int: provider = os.getenv("MINIFLOW_LLM", "mock").lower() print(f"MINIFLOW_LLM : {provider}") - if provider == "ollama": + print(f".env : {llm.envfile.LOADED[0] if llm.envfile.LOADED else '(none)'}") + if provider == "openai": + print(f"OPENAI_MODEL : {os.getenv('OPENAI_MODEL', 'gpt-4.1-mini')}") + print(f"API key : {'set' if os.getenv('OPENAI_API_KEY') else 'MISSING (put OPENAI_API_KEY in .env)'}") + if provider == "ollama" or os.getenv("MINIFLOW_LLM_FALLBACK", "").lower() == "ollama": print(f"OLLAMA_HOST : {os.getenv('OLLAMA_HOST', 'http://localhost:11434')}") print(f"OLLAMA_MODEL : {os.getenv('OLLAMA_MODEL', 'gemma3:4b')}") + if os.getenv("MINIFLOW_LLM_FALLBACK"): + print(f"FALLBACK : {os.getenv('MINIFLOW_LLM_FALLBACK')}") started = time.perf_counter() try: reply, used = llm.complete({"temperature": 0}, "Reply with exactly one word: ready", "", {}) diff --git a/Last3Mon/64/workflow-lab/src/miniflow/envfile.py b/Last3Mon/64/workflow-lab/src/miniflow/envfile.py new file mode 100644 index 0000000..ca0e044 --- /dev/null +++ b/Last3Mon/64/workflow-lab/src/miniflow/envfile.py @@ -0,0 +1,50 @@ +"""Load settings from the nearest .env file (KEY=VALUE lines, no extra package needed). + +Search order: the current folder and its parents, then the folders above this package +(so one Last3Mon/.env serves every day). Only the nearest file is read, and variables +already set in the shell win. Set MINIFLOW_DOTENV=0 to skip (the tests do this). +The .env file holds API keys: it is listed in .gitignore and must never be committed. +""" + +import os +from pathlib import Path + +LOADED: list[str] = [] # the .env file that was read, if any + + +def parse(text: str) -> dict[str, str]: + values = {} + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + key = key.removeprefix("export ").strip() + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": + value = value[1:-1] + elif " #" in value: + value = value.split(" #", 1)[0].rstrip() + values[key] = value + return values + + +def candidates() -> list[Path]: + paths: list[Path] = [] + for start in (Path.cwd(), Path(__file__).resolve().parent): + for folder in (start, *start.parents): + if folder / ".env" not in paths: + paths.append(folder / ".env") + return paths + + +def load() -> Path | None: + if os.getenv("MINIFLOW_DOTENV", "1") == "0": + return None + for path in candidates(): + if path.is_file(): + for key, value in parse(path.read_text(encoding="utf-8")).items(): + os.environ.setdefault(key, value) + LOADED.append(str(path)) + return path + return None diff --git a/Last3Mon/64/workflow-lab/src/miniflow/llm.py b/Last3Mon/64/workflow-lab/src/miniflow/llm.py index 5ea3e0b..eb8a51c 100644 --- a/Last3Mon/64/workflow-lab/src/miniflow/llm.py +++ b/Last3Mon/64/workflow-lab/src/miniflow/llm.py @@ -1,16 +1,21 @@ -"""LLM providers: mock (default, no API key), ollama (local), openai, anthropic, gemini. +"""LLM providers: mock (default, no API key), openai (course default, key in .env), ollama (local), anthropic, gemini. -Select with the MINIFLOW_LLM environment variable. +Select with MINIFLOW_LLM (usually set in .env). MINIFLOW_LLM_FALLBACK=ollama retries a failed call locally. """ import json import os +import sys +import time import urllib.error import urllib.request from typing import Any +from miniflow import envfile from miniflow.template import render +envfile.load() # OPENAI_API_KEY and other settings from .env + class LLMError(RuntimeError): pass @@ -111,26 +116,68 @@ def call_gemini(system: str, prompt: str, temperature: float) -> str: return response.text or "" -def call_openai(system: str, prompt: str, temperature: float) -> str: +def call_openai(system: str, prompt: str, temperature: float, schema: dict | None = None, + max_tokens: int | None = None) -> str: + """Call the OpenAI Responses API (OPENAI_MODEL, default gpt-4.1-mini). + + When `schema` is given, the answer is JSON that follows it (structured output). + A model that does not accept temperature is called again without it. + """ try: - from openai import OpenAI + import openai except ImportError as error: - raise LLMError("Install the extra first: uv sync --extra openai") from error + raise LLMError("The openai package is missing. Run: uv sync") from error if not os.getenv("OPENAI_API_KEY"): - raise LLMError("OPENAI_API_KEY is not set") - client = OpenAI() - response = client.responses.create( - model=os.getenv("OPENAI_MODEL", "gpt-4.1-mini"), - instructions=system or None, - input=prompt, - temperature=temperature, - ) - return response.output_text + raise LLMError("OPENAI_API_KEY is not set (put it in the .env file)") + client = openai.OpenAI(timeout=float(os.getenv("OPENAI_TIMEOUT", "60")), + max_retries=int(os.getenv("OPENAI_MAX_RETRIES", "2"))) + model = os.getenv("OPENAI_MODEL", "gpt-4.1-mini") + request: dict[str, Any] = {"model": model, "input": prompt, "temperature": temperature} + if system: + request["instructions"] = system + if max_tokens: + request["max_output_tokens"] = max(16, int(max_tokens)) # the API minimum is 16 + if schema: + request["text"] = {"format": {"type": "json_schema", "name": "reply", "schema": schema, "strict": False}} + started = time.perf_counter() + try: + try: + response = client.responses.create(**request) + except openai.BadRequestError as error: + if "temperature" not in str(error): + raise + request.pop("temperature") + response = client.responses.create(**request) + except openai.AuthenticationError as error: + raise LLMError("OpenAI rejected the API key (check OPENAI_API_KEY in .env)") from error + except openai.APIConnectionError as error: + raise LLMError(f"Cannot reach the OpenAI API ({error})") from error + except openai.APIStatusError as error: + raise LLMError(f"OpenAI API returned HTTP {error.status_code}: {error.message}") from error + return (response.output_text or "").strip() def complete(node: dict[str, Any], prompt: str, system: str, context: dict[str, Any]) -> tuple[str, str]: - """Return (reply, provider_name).""" - provider = os.getenv("MINIFLOW_LLM", "mock").lower() + """Return (reply, provider_name). + + With MINIFLOW_LLM_FALLBACK set (for example ollama), a call that fails on the main provider + is tried once more there, and the reply says which provider answered. + """ + primary = os.getenv("MINIFLOW_LLM", "mock").lower() + try: + return _complete_once(node, prompt, system, context, primary) + except LLMError as error: + fallback = os.getenv("MINIFLOW_LLM_FALLBACK", "").strip().lower() + if not fallback or fallback == primary or type(error).__name__ == "BudgetError": + raise + print(f"[miniflow] {primary} failed ({error}); trying {fallback}", file=sys.stderr) + try: + return _complete_once(node, prompt, system, context, fallback) + except LLMError as second: + raise LLMError(f"{primary} failed: {error} / fallback {fallback} failed: {second}") from second + + +def _complete_once(node: dict[str, Any], prompt: str, system: str, context: dict[str, Any], provider: str) -> tuple[str, str]: temperature = float(node.get("temperature", 0.2)) if provider == "mock": return mock_reply(prompt, node.get("mock"), context), "mock" @@ -141,5 +188,5 @@ def complete(node: dict[str, Any], prompt: str, system: str, context: dict[str, if provider == "gemini": return call_gemini(system, prompt, temperature), "gemini" if provider == "openai": - return call_openai(system, prompt, temperature), "openai" + return call_openai(system, prompt, temperature, node.get("json_schema")), "openai" raise LLMError(f"Unknown MINIFLOW_LLM value: {provider} (use mock, ollama, openai, anthropic or gemini)") diff --git a/Last3Mon/64/workflow-lab/tests/conftest.py b/Last3Mon/64/workflow-lab/tests/conftest.py new file mode 100644 index 0000000..aa78190 --- /dev/null +++ b/Last3Mon/64/workflow-lab/tests/conftest.py @@ -0,0 +1,3 @@ +import os + +os.environ["MINIFLOW_DOTENV"] = "0" # tests never read the real .env (it may hold API keys) diff --git a/Last3Mon/64/workflow-lab/tests/test_openai_provider.py b/Last3Mon/64/workflow-lab/tests/test_openai_provider.py new file mode 100644 index 0000000..1c33aea --- /dev/null +++ b/Last3Mon/64/workflow-lab/tests/test_openai_provider.py @@ -0,0 +1,80 @@ +"""OpenAI provider, .env loading and the Ollama fallback, without network access.""" + +import types + +import pytest + +from miniflow import envfile, llm + + +def test_parse_env_lines(): + values = envfile.parse('# comment\nexport A=1\nB="two words"\nC=3 # note\n\nbroken line\n') + assert values == {"A": "1", "B": "two words", "C": "3"} + + +def test_load_keeps_shell_values(tmp_path, monkeypatch): + (tmp_path / ".env").write_text("MINIFLOW_TEST_A=from-file\nMINIFLOW_TEST_B=from-file\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("MINIFLOW_DOTENV", "1") + monkeypatch.setenv("MINIFLOW_TEST_A", "from-shell") + monkeypatch.delenv("MINIFLOW_TEST_B", raising=False) + assert envfile.load() == tmp_path / ".env" + assert llm.os.environ["MINIFLOW_TEST_A"] == "from-shell" + assert llm.os.environ["MINIFLOW_TEST_B"] == "from-file" + monkeypatch.delenv("MINIFLOW_TEST_B") + + +def test_openai_without_key(monkeypatch): + monkeypatch.setenv("MINIFLOW_LLM", "openai") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("MINIFLOW_LLM_FALLBACK", raising=False) + with pytest.raises(llm.LLMError, match="OPENAI_API_KEY"): + llm.complete({}, "hello", "", {}) + + +def test_openai_request_shape(monkeypatch): + sent = {} + + class FakeResponses: + def create(self, **kwargs): + sent.update(kwargs) + return types.SimpleNamespace(output_text='{"ok": true}', usage=None) + + class FakeClient: + def __init__(self, **_): + self.responses = FakeResponses() + + import openai + + monkeypatch.setattr(openai, "OpenAI", FakeClient) + monkeypatch.setenv("MINIFLOW_LLM", "openai") + monkeypatch.setenv("OPENAI_API_KEY", "test-key") + monkeypatch.setenv("OPENAI_MODEL", "test-model") + schema = {"type": "object", "properties": {"ok": {"type": "boolean"}}} + reply, used = llm.complete({"temperature": 0, "json_schema": schema}, "hello", "be brief", {}) + assert (reply, used) == ('{"ok": true}', "openai") + assert sent["model"] == "test-model" and sent["instructions"] == "be brief" and sent["temperature"] == 0 + assert sent["text"]["format"]["schema"] == schema + + +def test_fallback_to_second_provider(monkeypatch, capsys): + monkeypatch.setenv("MINIFLOW_LLM", "openai") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setenv("MINIFLOW_LLM_FALLBACK", "ollama") + monkeypatch.setattr(llm, "call_ollama", lambda *args, **kwargs: "from ollama") + reply, used = llm.complete({}, "hello", "", {}) + assert (reply, used) == ("from ollama", "ollama") + assert "openai failed" in capsys.readouterr().err + + +def test_both_providers_fail(monkeypatch): + monkeypatch.setenv("MINIFLOW_LLM", "openai") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setenv("MINIFLOW_LLM_FALLBACK", "ollama") + + def broken(*args, **kwargs): + raise llm.LLMError("Cannot reach Ollama") + + monkeypatch.setattr(llm, "call_ollama", broken) + with pytest.raises(llm.LLMError, match="OPENAI_API_KEY.*fallback ollama failed"): + llm.complete({}, "hello", "", {}) diff --git a/Last3Mon/64/workflow-lab/uv.lock b/Last3Mon/64/workflow-lab/uv.lock index 034f1f2..73f1e10 100644 --- a/Last3Mon/64/workflow-lab/uv.lock +++ b/Last3Mon/64/workflow-lab/uv.lock @@ -647,6 +647,9 @@ wheels = [ name = "miniflow" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "openai" }, +] [package.optional-dependencies] anthropic = [ @@ -668,6 +671,7 @@ dev = [ requires-dist = [ { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=1.13" }, { name = "google-genai", marker = "extra == 'gemini'", specifier = ">=1.0" }, + { name = "openai", specifier = ">=1.40" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.40" }, ] provides-extras = ["gemini", "openai", "anthropic"] diff --git a/Lecture/64/00-DAY64-OVERVIEW.pptx b/Lecture/64/00-DAY64-OVERVIEW.pptx index 6ae1ba3..dbb81ec 100644 Binary files a/Lecture/64/00-DAY64-OVERVIEW.pptx and b/Lecture/64/00-DAY64-OVERVIEW.pptx differ diff --git a/Lecture/64/00-DAY64-PREP.md b/Lecture/64/00-DAY64-PREP.md index 411e21e..b12374f 100644 --- a/Lecture/64/00-DAY64-PREP.md +++ b/Lecture/64/00-DAY64-PREP.md @@ -6,7 +6,7 @@ ```mermaid flowchart LR - A["1. 실습 환경
WSL · uv · Ollama"] --> B["2. 확인 명령
pytest · check"] + A["1. 실습 환경
WSL · uv · API 키 · Ollama"] --> B["2. 확인 명령
pytest · check"] B --> C["3. 미리 읽기
JSON · 지난 실습"] C --> D["4. 생각해 오기
내 업무 시나리오"] ``` @@ -15,19 +15,20 @@ flowchart LR ## 1. 실습 환경 준비 (전날 필수, 약 40분) -오늘 실습은 Windows 11 노트북의 **WSL(Ubuntu)** 에서, LLM은 노트북에서 직접 실행하는 **Ollama**로 진행합니다. API 키는 필요 없습니다. +오늘 실습은 Windows 11 노트북의 **WSL(Ubuntu)** 에서 진행합니다. LLM은 과정에서 받은 키로 **OpenAI API**(`gpt-4.1-mini`)를 쓰고, 키·인터넷에 문제가 있으면 노트북에서 직접 실행하는 **Ollama**(`gemma3:4b`)가 대신 답합니다. 이 구성은 65일차 이후에도 그대로 씁니다. -자세한 순서는 **[사전 준비 안내 (SETUP-WSL-OLLAMA)](../../Last3Mon/64/SETUP-WSL-OLLAMA.md)** 를 따라 하세요. 할 일만 요약하면 다음과 같습니다. +자세한 순서는 **[사전 준비 안내 (SETUP-WSL-LLM)](../../Last3Mon/64/SETUP-WSL-LLM.md)** 를 따라 하세요. 할 일만 요약하면 다음과 같습니다. | 순서 | 할 일 | 시간 | | --- | --- | --- | | 1 | WSL(Ubuntu) 동작 확인 | 5분 | -| 2 | 노트북 메모리 확인 → 사용할 모델 결정 (8GB: `gemma3:1b`, 16GB 이상: `gemma3:4b`) | 2분 | -| 3 | uv 설치, 수업 저장소 받기 (WSL 홈 `~` 아래) | 10분 | -| 4 | Ollama 설치와 모델 다운로드 | 10~20분 | -| 5 | `~/.bashrc`에 `MINIFLOW_LLM`, `OLLAMA_MODEL` 설정 | 3분 | +| 2 | uv 설치, 수업 자료 받기 (WSL 홈 `~` 아래) | 10분 | +| 3 | `Last3Mon/.env`에 OpenAI API 키 넣기 | 3분 | +| 4 | 노트북 메모리 확인 → 대체 모델 결정 (8GB: `gemma3:1b`, 16GB 이상: `gemma3:4b`) | 2분 | +| 5 | Ollama 설치와 모델 다운로드 (대체용) | 10~20분 | +| 6 | 연결 확인 (OpenAI, 대체 Ollama) | 5분 | -> 모델 파일이 1~3GB라서 수업 중에 여러 명이 동시에 받으면 강의실 네트워크가 매우 느려집니다. **다운로드는 꼭 집에서 미리** 해 주세요. +> Ollama 모델 파일이 1~3GB라서 수업 중에 여러 명이 동시에 받으면 강의실 네트워크가 매우 느려집니다. **다운로드는 꼭 집에서 미리** 해 주세요. ## 2. 준비가 끝났는지 확인 (전날 필수, 5분) @@ -35,17 +36,19 @@ WSL 터미널에서 실습 폴더로 이동해 아래 두 명령을 실행합니 ```bash cd ~/<수업 저장소>/Last3Mon/64/workflow-lab -uv run pytest -q # 12 passed, 1 skipped 가 나오면 성공 -uv run miniflow check # Reply (ollama, ...) 가 나오면 성공 +uv run pytest -q # 18 passed, 1 skipped 가 나오면 성공 +uv run miniflow check # Reply (openai, ...) 가 나오면 성공 +OPENAI_API_KEY=sk-wrong uv run miniflow check # Reply (ollama, ...) 가 나오면 대체도 준비 완료 ``` | 결과 | 의미 | | --- | --- | -| 두 명령 모두 성공 | 준비 완료 | -| `pytest`는 성공, `check`가 실패 | Ollama 설정 문제. 안내서 7절(문제 해결) 확인 | +| 세 명령 모두 성공 | 준비 완료 | +| `pytest`는 성공, 첫 `check`가 실패 | `.env`의 키 문제. 안내서 8절(문제 해결) 확인 | +| 첫 `check`는 성공, 두 번째가 실패 | 대체용 Ollama 문제. 수업은 OpenAI로 할 수 있으니 1교시 전에 말씀해 주세요 | | 둘 다 실패 | uv 또는 저장소 위치 문제. 안내서 3절부터 다시 확인 | -해결이 안 되면 수업 당일 1교시 전에 말씀해 주세요. 그동안은 `export MINIFLOW_LLM=mock`으로 Mock LLM을 써서 실습을 따라올 수 있습니다. +해결이 안 되면 수업 당일 1교시 전에 말씀해 주세요. 키를 아직 못 받았으면 `.env`의 `MINIFLOW_LLM=ollama`로, 둘 다 안 되면 `export MINIFLOW_LLM=mock`으로 Mock LLM을 써서 실습을 따라올 수 있습니다. --- @@ -118,14 +121,16 @@ uv run miniflow check # Reply (ollama, ...) 가 나오면 성공 ## 5. 당일 준비물 -- 충전된 노트북과 충전기 (Ollama는 배터리를 많이 사용합니다) +- 충전된 노트북과 충전기 (대체용 Ollama는 배터리를 많이 사용합니다) - Windows Terminal (WSL 터미널), VS Code - 웹 브라우저 (워크플로우 그림 확인용 [Mermaid Live Editor](https://mermaid.live)) -- (선택) OpenAI/Anthropic/Gemini 중 가지고 있는 API 키 하나 (예: 10일차에 발급한 Gemini 키) — 로컬 모델과 클라우드 모델을 비교해 볼 수 있습니다 +- 과정에서 받은 OpenAI API 키 (`Last3Mon/.env`에 넣어 오기). 키는 채팅·문서·화면 캡처에 올리지 않습니다 +- (선택) Anthropic·Gemini 키가 있으면 (예: 10일차에 발급한 Gemini 키) 같은 워크플로우를 다른 제공사 모델로도 비교해 볼 수 있습니다 ## 확인 -- [ ] `uv run pytest -q` → `12 passed, 1 skipped` -- [ ] `uv run miniflow check` → `Reply (ollama, ...)` +- [ ] `uv run pytest -q` → `18 passed, 1 skipped` +- [ ] `uv run miniflow check` → `Reply (openai, ...)` +- [ ] `OPENAI_API_KEY=sk-wrong uv run miniflow check` → `Reply (ollama, ...)` - [ ] JSON 기초(3.1)를 읽었다 - [ ] Lab 03에서 분석할 업무 후보를 하나 생각했다