D-2 수정 작업: OpenAI API 우선 없으면 Ollama 적용
This commit is contained in:
1 parent
5eedbdd039
commit
2fe0dc8de1
16 files changed
+466
-218
No files matched your search
@@ -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
|
||||
|
||||
@@ -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의 결과·속도 차이를 표로 정리했다.
|
||||
@@ -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` 결과를 분석 문서의 "구현한 워크플로우" 항목에 붙여 넣습니다.
|
||||
|
||||
|
||||
@@ -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, ...) : 대체 경로
|
||||
```
|
||||
@@ -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<br/>OPENAI_API_KEY<br/>(git 제외)"] -.-> M
|
||||
M["miniflow<br/>(uv 프로젝트)"] -.->|"대체<br/>localhost:11434"| O["Ollama 서버"]
|
||||
O --> G[("gemma3:4b 모델")]
|
||||
end
|
||||
T --> WSL
|
||||
end
|
||||
M -->|"기본"| AI["OpenAI API<br/>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, ...)`가 나온다
|
||||
@@ -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<br/>(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, ...)`가 나온다
|
||||
@@ -5,7 +5,7 @@
|
||||
```mermaid
|
||||
flowchart LR
|
||||
J["flows/*.json<br/>노드와 연결 정의"] --> E["miniflow 엔진<br/>순서 계산 · 실행"]
|
||||
E --> L{{"LLM 노드<br/>ollama · mock · openai · anthropic · gemini"}}
|
||||
E --> L{{"LLM 노드<br/>openai · ollama · mock · anthropic · gemini"}}
|
||||
E --> T["Tool 노드<br/>get_weather · search_faq"]
|
||||
E --> M[("Memory 노드<br/>.miniflow-memory.json")]
|
||||
E --> R["runs/*.json<br/>실행 기록(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 # 대체 모델만 따로
|
||||
```
|
||||
@@ -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"]
|
||||
|
||||
@@ -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", "", {})
|
||||
|
||||
@@ -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
|
||||
@@ -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)")
|
||||
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
|
||||
os.environ["MINIFLOW_DOTENV"] = "0" # tests never read the real .env (it may hold API keys)
|
||||
@@ -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", "", {})
|
||||
Generated
+4
@@ -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"]
|
||||
|
||||
Binary file not shown.
+22
-17
@@ -6,7 +6,7 @@
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["1. 실습 환경<br/>WSL · uv · Ollama"] --> B["2. 확인 명령<br/>pytest · check"]
|
||||
A["1. 실습 환경<br/>WSL · uv · API 키 · Ollama"] --> B["2. 확인 명령<br/>pytest · check"]
|
||||
B --> C["3. 미리 읽기<br/>JSON · 지난 실습"]
|
||||
C --> D["4. 생각해 오기<br/>내 업무 시나리오"]
|
||||
```
|
||||
@@ -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에서 분석할 업무 후보를 하나 생각했다
|
||||
Reference in new issue
Block a user