From 2fe0dc8de1181f9df64105f417d677d888f6b8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EB=AC=B8=EC=B0=BD=40MBA?= Date: Mon, 12 Oct 2026 22:29:38 +0900 Subject: [PATCH] =?UTF-8?q?D-2=20=EC=88=98=EC=A0=95=20=EC=9E=91=EC=97=85:?= =?UTF-8?q?=20OpenAI=20API=20=EC=9A=B0=EC=84=A0=20=EC=97=86=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20Ollama=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Last3Mon/64/LAB-01-FIRST-WORKFLOW.md | 22 ++- Last3Mon/64/LAB-02-LLM-TOOL-MEMORY.md | 31 +-- Last3Mon/64/LAB-03-SCENARIO-ANALYSIS.md | 11 +- Last3Mon/64/README.md | 11 +- Last3Mon/64/SETUP-WSL-LLM.md | 181 ++++++++++++++++++ Last3Mon/64/SETUP-WSL-OLLAMA.md | 144 -------------- Last3Mon/64/workflow-lab/README.md | 17 +- Last3Mon/64/workflow-lab/pyproject.toml | 2 +- Last3Mon/64/workflow-lab/src/miniflow/cli.py | 8 +- .../64/workflow-lab/src/miniflow/envfile.py | 50 +++++ Last3Mon/64/workflow-lab/src/miniflow/llm.py | 81 ++++++-- Last3Mon/64/workflow-lab/tests/conftest.py | 3 + .../tests/test_openai_provider.py | 80 ++++++++ Last3Mon/64/workflow-lab/uv.lock | 4 + Lecture/64/00-DAY64-OVERVIEW.pptx | Bin 64760 -> 64781 bytes Lecture/64/00-DAY64-PREP.md | 39 ++-- 16 files changed, 466 insertions(+), 218 deletions(-) create mode 100644 Last3Mon/64/SETUP-WSL-LLM.md delete mode 100644 Last3Mon/64/SETUP-WSL-OLLAMA.md create mode 100644 Last3Mon/64/workflow-lab/src/miniflow/envfile.py create mode 100644 Last3Mon/64/workflow-lab/tests/conftest.py create mode 100644 Last3Mon/64/workflow-lab/tests/test_openai_provider.py 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 6ae1ba3ab2a87c4ce49bc8ff56e105b363bae4a7..dbb81eca2dfca39fc4660432dbad904bfacbd30c 100644 GIT binary patch delta 8750 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 · 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에서 분석할 업무 후보를 하나 생각했다