diff --git a/Last3Mon/74/LAB-01-A-WHAT-CHANGES.md b/Last3Mon/74/LAB-01-A-WHAT-CHANGES.md new file mode 100644 index 0000000..b9ef9df --- /dev/null +++ b/Last3Mon/74/LAB-01-A-WHAT-CHANGES.md @@ -0,0 +1,115 @@ +# Lab 01 · 무엇이 바뀌었나, 답마다 버전, 지시문은 저장소로 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-01-Q-WHAT-CHANGES.md](LAB-01-Q-WHAT-CHANGES.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 실습을 5개로 늘린 뒤 정답 워크플로우를 학생 순서대로 2026-10-27에 1회씩 실행한 값입니다 (강사 Mac, `gpt-4.1-mini` · `gpt-5.4-nano`, bench는 n=5). 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). + +## 예제 1 · 결과 (에이전트 없이) + +| 바뀐 곳 (`diff`, 7개) | 전 | 후 | 셋 중 | +| --- | --- | --- | --- | +| `Answer` 지시문 | 307자 | 419자 (+ "FAQ에서 온 답이면 끝에 `[FAQ-003]`", "Keep answers short.") | 지시문 | +| `Answer model` | `gpt-4.1-mini` | `gpt-5.4-nano` | 모델 | +| 노드 `Review check` + 연결 셋 | 없음 | 답을 400자에서 자름 | 흐름 | +| 웹훅 경로 | `rel-v1` | `rel-v2` | 흐름 (답에는 영향 없음) | + +| `call` (`answers/02`) | 시간 | 답 | +| --- | --- | --- | +| `rel-v1` | 2.8초 | "… 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다 …" | +| `rel-v2` | 1.0초 | "… 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. **[FAQ-003]**" | + +- 풀이 때: "출처 번호는 어디 때문?" → 지시문일 가능성이 높지만 **이 비교만으로는 확정 못 함** (모델도 바뀜). "빨라진 것은?" → 모델일 것 같지만 마찬가지. +- **수업 준비 중 실제로 본 것**: 10/18 판 v2는 `gpt-4.1-nano`였고 10/27에 OpenAI가 지원 종료해 HTTP 500이 났습니다. 워크플로우를 하나도 안 바꿨는데 멈춘 실제 예라 1단계 2절에 넣었습니다. + +## 예제 2 · 요청문 예 + +```text +my-work/helpdesk.json (v1 을 복사한 것) 의 웹훅 경로를 my-helpdesk 로, 이름을 "74 IT helpdesk (my)" 로 바꿔 줘. +Has question? 뒤에 Release info(Set) 를 넣어 release 1.0.0, prompt_version p1, model gpt-4.1-mini. +Answer model 의 모델 값은 ={{ $('Release info').first().json.model }} 로 Release info 의 model 을 읽게. +Build result 결과에 version : {release, prompt_version, model} 을 Release info 에서 더해 줘. 지시문은 그대로 . +기대 결과 cases/lab01.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `my-helpdesk` | `cases/lab01.json`의 `webhook` (AGENTS.md 3절) | +| ② | `={{ $('Release info').first().json.model }}` | 앞 노드 값을 읽는 n8n 표현식, 모델 이름을 한 곳에서 | +| ③ | `version` | AGENTS.md 3절 결과 모양의 버전 세 칸을 감싸는 이름 | +| ④ | 그대로 | 지시문까지 바꾸면 무엇 때문에 답이 바뀌었는지 또 모름 | + +정답: [reference/lab01-example2-version.json](reference/lab01-example2-version.json) + +| 10/27 결과 | | +| --- | --- | +| `cases/lab01.json` (`answers/03`) | **3/3** | + +## 예제 3 · 결과 (에이전트 없이) + +| 재기 | v1 (`rel-v1`) | v2 (`rel-v2`) | +| --- | --- | --- | +| `lab01-compare --repeat 3` (`answers/04` · `05`) | **9/15** — 출처 번호 두 케이스가 세 번 모두 ✘ | **15/15** | +| `bench -n 5` VPN (`answers/06`) | p50 1.23초 · p95 1.34초 | p50 0.98초 · p95 1.20초 | + +- 출처 번호는 세 번 모두 같은 쪽 → **흔들림이 아니라 바뀐 것**. 해결 낱말 · 접수 현황은 둘 다 세 번 모두 ✔ (흔들림 없음). +- 시간 차이는 0.25초 정도. 모델 · 지시문 · 노드가 함께 바뀌어 원인을 말할 수 없습니다 → Lab 02의 한 가지씩 바꾼 릴리스로 이어 갑니다. + +## 과제 1 · 요청문 예 + +```text +my-work/helpdesk.json 의 Release info 뒤에 Get prompt(HTTP GET) 를 넣어 줘. +주소는 http://data-api:8800/prompts/helpdesk/ prompt_version (Release info 의 값을 표현식으로). +Answer 의 systemMessage 는 지금 글자 대신 Get prompt 의 text 를 표현식으로 읽게. +Build result 의 version 에 prompt_source : ' registry ' 를 더해 줘. 다른 것은 그대로. +기대 결과 cases/lab01-challenge.json, cases/lab01.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `prompt_version` | 릴리스 정보 세 값 중 지시문 버전 이름 (`{{ $('Release info').first().json.prompt_version }}`) | +| ② | `Get prompt` | 지시문 글자를 가져오는 노드 | +| ③ | `prompt_source` | 지시문을 어디서 가져왔는지 (AGENTS.md 3절) | +| ④ | `registry` | `cases/lab01-challenge.json`의 기대 | + +정답: [answers/74-lab01-challenge-registry.json](answers/74-lab01-challenge-registry.json) + +| 10/27 결과 | 고치기 전 (예제 2 정답) | 고친 뒤 | +| --- | --- | --- | +| `cases/lab01-challenge.json` (`answers/07` · `08`) | 2/3 (`prompt_source` 없음) | **3/3** | +| `cases/lab01.json` (`answers/09`) | | 3/3 | + +## 과제 2 · 요청문 예 + +```text +my-work/helpdesk.json 의 Get prompt 에 options.timeout 2000 과 onError continueErrorOutput 를 넣어 줘. +성공 쪽은 Registry prompt(Set, text = $json.text), 실패 쪽은 Fallback prompt(Set, text = data-api/data/prompts/helpdesk- p1 .md 와 같은 글). +두 Set 모두 Search FAQ 로 이어 줘. Answer 의 systemMessage 는 Registry prompt 가 실행됐으면 그 text, 아니면 Fallback prompt 의 text. +version.prompt_source 는 Registry prompt 가 실행됐으면 'registry', 아니면 ' fallback '. +기대 결과 cases/lab01-challenge2.json, cases/lab01-challenge.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `2000` | 2초를 ms로 | +| ② | `continueErrorOutput` | 실패해도 멈추지 않고 두 번째(오류) 출력으로 (72일차 Lab 03, 73일차 Lab 02 과제 2와 같음) | +| ③ | `p1` | 마지막으로 검증된 지시문 (운영 중인 1.0.0의 지시문) | +| ④ | `fallback` | `cases/lab01-challenge2.json`의 고장 케이스 기대 | + +정답: [answers/74-lab01-challenge2-fallback.json](answers/74-lab01-challenge2-fallback.json) (과제 1 정답 + `Get prompt`의 `timeout` · `onError`, 노드 2개: `Registry prompt` · `Fallback prompt`) + +| 10/27 결과 (`cases/lab01-challenge2.json`) | 고치기 전 (과제 1 정답, `answers/10`) | 고친 뒤 (`answers/11`) | +| --- | --- | --- | +| 평소 · 저장소에서 | ✔ | ✔ `registry` | +| 저장소 고장 (`prompts error`) | ✘ **HTTP 500** `Error in workflow` (0.1초) | ✔ 200 · `fallback` · "인증서" (1.3초) | +| 저장소 느림 (`prompts slow` 8초) | ✘ **9.1초** (8초를 다 기다림, `registry`) | ✔ `fallback` · 3.0초 | +| 되돌린 뒤 | ✔ | ✔ `registry` | +| 합계 | 2/4 | **4/4** | +| `lab01-challenge` (`answers/12`) | | 3/3 | + +- 풀이 때 "73일차 판별 LLM은 막았는데 왜 여기는 계속 가나?"를 묻습니다: 판별 LLM은 보안 관문(멈추면 공격이 지나감), 지시문 저장소는 편의 의존(멈춰도 예전 지시문으로 답할 뿐). +- **다음 단계로 넘기는 질문**: "저장소가 404(그런 지시문 없음)를 주면 이 기본 지시문은 어떻게 할까?" → Lab 02 과제 2. diff --git a/Last3Mon/74/LAB-02-A-RELEASE-GATE.md b/Last3Mon/74/LAB-02-A-RELEASE-GATE.md new file mode 100644 index 0000000..872f642 --- /dev/null +++ b/Last3Mon/74/LAB-02-A-RELEASE-GATE.md @@ -0,0 +1,119 @@ +# Lab 02 · 릴리스 고르기, 후보 시험, 배포 전 게이트 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-02-Q-RELEASE-GATE.md](LAB-02-Q-RELEASE-GATE.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 학생 순서대로 2026-10-27에 1회씩 실행한 값입니다 (강사 Mac, `gpt-4.1-mini` · `gpt-5.4-nano`, bench는 n=5). 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). + +## 예제 1 · 요청문 예 + +```text +my-work/helpdesk.json 의 Release info(Set) 를 Get release(HTTP GET http://data-api:8800/releases/helpdesk) → Pick release(Code) → Known release?(If) 로 바꿔 줘. +Pick release: 본문 release 가 있으면 그 릴리스 · channel pinned , 없으면 active · channel stable . 고른 릴리스의 prompt_version · model 을 돌려준다. +releases 에 없으면 Known release? false → Reject release(400, {error: 'unknown release', release, known }). +Get prompt · Answer model · Build result 는 Release info 대신 Pick release 를 읽게. 기본 지시문(Fallback prompt)은 그대로. +기대 결과 cases/lab02.json, cases/lab01-challenge.json, cases/lab01-challenge2.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `pinned` | 본문 `release`로 고정 | +| ② | `stable` | 운영 중인 `active` | +| ③ | `known` | 알려진 릴리스 목록 (`cases/lab02.json` "없는 릴리스" 기대) | +| ④ | `Pick release` | 릴리스를 고른 노드 | + +정답: [reference/lab02-example1-release.json](reference/lab02-example1-release.json) (Lab 01 과제 1 · 2 포함) + +| 10/27 결과 | 고치기 전 (Lab 01 과제 2 정답, `answers/13`) | 고친 뒤 (`answers/14` · `15`) | +| --- | --- | --- | +| `cases/lab02.json` | 1/5 (고정이 안 먹음, 9.9.9도 200, `channel` 없음) | **5/5** | +| `lab01-challenge` · `lab01-challenge2` | | 3/3 · 4/4 | + +## 예제 2 · 결과 (에이전트 없이) + +| 약한 golden (`answers/16`) | 결과 | p95 | +| --- | --- | --- | +| 1.0.0 | 6/6 PASS | 1.6초 | +| 1.1.0 | 6/6 PASS | 1.1초 | +| **1.2.0** | **6/6 PASS** | 1.5초 | +| 2.0.0 | 6/6 PASS | 2.0초 | + +- 1.2.0 프린터 답 (`call -v`): **"IT 포털에서 프린터 문제 해결 방법을 확인해 주세요."** 내보내면 안 되는 답이 PASS. + +## 예제 3 · 결과 (에이전트 없이) + +| 재기 (`answers/17`) | 결과 | +| --- | --- | +| VPN `bench -n 5 --set release=1.0.0` | p50 **1.23초** · p95 1.42초 | +| VPN `bench -n 5 --set release=2.0.0` | p50 **1.12초** · p95 1.24초 | +| `gate golden --set release=1.0.0 --repeat 3` | **18/18** PASS, p95 1.5초 | + +- 기준선 한 줄 예: "1.0.0 p95 1.4초 근처, 3번 반복에도 흔들림 없음 → `--max-p95 3`은 약 두 배 여유, 통과율 100%를 그대로 기준으로." +- 2.0.0은 1.1.0과 지시문이 같고 모델만 다릅니다. 빨라진 0.1초는 모델 때문이라고 말할 수 있습니다 (한 가지만 바꿨으니). 다만 5번이라 차이가 작으면 흔들림 안쪽일 수 있다고 짚습니다. + +## 과제 1 · 요청문 예 + +```text +cases/golden.json 을 cases/my-golden.json 으로 복사해 expect 만 강하게 바꿔 줘. 워크플로우는 바꾸지 않는다 . +질문마다 답에 꼭 있어야 할 낱말을 contains 로 (아래는 내가 FAQ · 접수 건 데이터에서 정한 것): +VPN 인증서 / VPN 업데이트 · 최신 / 비밀번호 비밀번호 찾기 · 휴대폰 / 프린터 대기열 · 전원 / 메일함 보관함 · 삭제 / +T-1002 처리 중 / T-1002 10월 20일 · 20일 / T-1001 처리 완료 · 완료. +프린터 질문에는 " IT 포털을 확인 · 포털을 참고 " 같은 떠넘기기 말이 없어야 한다 (not_contains). +기대: gate --set release=1.0.0 · 1.1.0 PASS, release= 1.2.0 FAIL. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | 바꾸지 않는다 | 과제 조건: 테스트만 고친다 | +| ② | `contains` | 목록 중 하나라도 있으면 통과 (AGENTS.md 8절) | +| ③ | `IT 포털을 확인` · `포털을 참고` (정답 파일) | 1.2.0 지시문(p3)이 "IT 포털을 보라"고 시킴. **실제 답 "포털에서 … 확인해 주세요"는 이 글자에 안 걸림** → 풀이 거리 | +| ④ | `1.2.0` | 막아야 할 릴리스 | + +정답 (테스트 파일): [answers/74-lab02-challenge-golden.json](answers/74-lab02-challenge-golden.json) (9케이스) + +| 10/27 결과 (`answers/18`) | 강한 golden | p95 | +| --- | --- | --- | +| 1.0.0 | **9/9 PASS** | 1.4초 | +| 1.1.0 | **9/9 PASS** | 1.7초 | +| 1.2.0 | **1/9 FAIL** | 1.3초 | +| 2.0.0 | **9/9 PASS** | 1.3초 | + +- 1.2.0이 통과한 하나 = "포털로 떠넘기지 않기"(`not_contains`). 해결 낱말 8케이스가 막았습니다. +- 학생이 ③에 "포털"만 적으면: 프린터 케이스에만 걸면 1.2.0의 그 케이스도 ✘가 됩니다 (1.0.0 · 1.1.0의 프린터 답에는 "포털"이 없었음). 그런데 **모든 질문에** `not_contains "포털"`을 걸면 1.0.0 · 1.1.0도 FAIL — 비밀번호 답이 "**사내 포털** 로그인 화면의 '비밀번호 찾기'"라고 정상으로 씁니다 (`runs/74-lab02-challenge-golden-*.json`의 1.0.0 · 1.1.0 응답). 금지 낱말이 넓으면 지나치게 막는다는 73일차 이야기와 같습니다. 어느 쪽이든 1.2.0은 FAIL. +- 2.0.0 판단 예: "강한 golden PASS, p95 1.3초 → 카나리 10%로 먼저, 비용(75일차) 본 뒤 배포." + +## 과제 2 · 요청문 예 + +```text +my-work/helpdesk.json 의 Get prompt 에 options.response.response 로 neverError true, fullResponse true 를 더해 줘 (timeout 2000 · onError 는 그대로). +성공 쪽 Registry prompt 를 Code 로 바꿔: statusCode 200 이면 {state: 'registry', text: body.text}, 404 이면 {state: 'broken'}, 그 밖이면 {state: 'fallback', text: 기본 지시문}. +Prompt broken?(If, state == 'broken') true → Reject broken(HTTP 400 , {error: 'release broken', release, prompt_version}), false → Search FAQ. +prompt_source 는 Registry prompt 의 state 가 'registry' 면 registry, 아니면 fallback. 실패 쪽(시간 초과) Fallback prompt 는 그대로 둔다 . +기대 결과 cases/lab02-challenge2.json, cases/lab02.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `fullResponse` | 응답 코드 · 본문을 같이 받음 (`$json.statusCode`) | +| ② | `404` | `/prompts/helpdesk/p9` → 404 `prompt not found` | +| ③ | `400` | `cases/lab02-challenge2.json` 첫 케이스 `status` | +| ④ | 그대로 둔다 | 장애(시간 초과)는 지금처럼 견딤 | + +정답: [answers/74-lab02-challenge2-broken.json](answers/74-lab02-challenge2-broken.json) (예제 1 정답 + `Get prompt` 응답 옵션, `Registry prompt`를 Code로, 노드 2개: `Prompt broken?` · `Reject broken`) + +| 10/27 결과 | 고치기 전 (예제 1 정답, `answers/19`) | 고친 뒤 (`answers/20`) | +| --- | --- | --- | +| 망가진 릴리스 2.1.0 | ✘ **200** (기본 지시문 p1으로 답) | ✔ 400 `release broken` · `p9` | +| 평소 · 1.1.0 | ✔ | ✔ `registry` · `[FAQ-003]` | +| 저장소 고장 · 1.1.0 | ✔ `fallback` | ✔ `fallback` | +| 저장소 고장 · 9.9.9 | ✔ 400 | ✔ 400 | +| 운영 1.0.0 | ✔ | ✔ | +| 합계 | 4/5 | **5/5** | +| `gate my-golden --set release=2.1.0` | **9/9 PASS** (p95 1.5초) | **0/9 FAIL** | +| `lab02` · `lab01-challenge2` (`answers/21`) | | 5/5 · 4/4 | + +- 풀이 핵심: **장애를 견디는 장치(Lab 01 과제 2)가 설정 오류를 숨겼고, 그래서 게이트도 PASS를 줬다.** 4xx는 크게 실패(fail fast), 5xx · 시간 초과만 견딘다 — 72일차 재시도 원칙과 같은 구분. diff --git a/Last3Mon/74/LAB-03-A-DEPLOY-AND-ROLLBACK.md b/Last3Mon/74/LAB-03-A-DEPLOY-AND-ROLLBACK.md new file mode 100644 index 0000000..d9a3f1c --- /dev/null +++ b/Last3Mon/74/LAB-03-A-DEPLOY-AND-ROLLBACK.md @@ -0,0 +1,120 @@ +# Lab 03 · 배포 · 되돌리기, 카나리, 릴리스 점검 워크플로우 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-03-Q-DEPLOY-AND-ROLLBACK.md](LAB-03-Q-DEPLOY-AND-ROLLBACK.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 학생 순서대로 2026-10-27에 1회씩 실행한 값입니다 (강사 Mac, `gpt-4.1-mini` · `gpt-5.4-nano`). 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). + +## 예제 1 · 결과 (에이전트 없이) + +| 재기 (`answers/22`) | 결과 | +| --- | --- | +| `release helpdesk 1.1.0` → `call` | `version.release` 1.1.0 · `stable`, 답 끝 `[FAQ-003]` | +| `release helpdesk 1.0.0` → `call` | 1.0.0, 출처 번호 없음 | +| `cases/lab03.json` | **2/2** | + +- push · n8n 재시작 없음. `RELEASE.md` 3절 예: "1.1.0 배포 → 1.0.0 되돌림, 명령 한 줄, 다음 요청부터, `call -v`의 `version.release`로 확인". + +## 예제 2 · 요청문 예 + +```text +my-work/helpdesk.json 의 Pick release 에 카나리를 더해 줘. +bucket = user 이름 글자의 codePointAt 합 % 100 . 본문 release 가 없고 canary 가 있고 bucket < canary.percent 이면 +canary.release 를 쓰고 channel ' canary '. 순서는 고정 > 카나리 > active ( 고정(본문 release) 가 맨 먼저). +기대 결과 cases/lab03-canary.json, cases/lab02.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `100` | AGENTS.md 3절 카나리 대상 규칙 (`% 100`) | +| ② | `<` | 작으면 카나리 (같으면 들어가지 않음) | +| ③ | `canary` | `channel` 세 값 중 하나 | +| ④ | 고정 (본문 `release`) | 테스트 · 점검이 카나리에 섞이지 않게 | + +정답: [reference/lab03-example2-canary.json](reference/lab03-example2-canary.json) (Lab 01 · 02 과제 포함) + +| 10/27 결과 | 고치기 전 (Lab 02 과제 2 정답, `answers/23`) | 고친 뒤 (`answers/24`) | +| --- | --- | --- | +| `cases/lab03-canary.json` | 3/6 (카나리 대상 둘 · "같은 사람" 이 `stable`) | **6/6** | +| `lab02` · `lab02-challenge2` | | 5/5 · 5/5 | + +## 예제 3 · 결과 (에이전트 없이) + +| 재기 | 결과 | +| --- | --- | +| `lab03-watch` (1.2.0 50% 카나리, `answers/25`) | **4/6** — 최유진 · 정우성 ✘ ("대기열" 없는 한 문장 답), 운영 넷 ✔ | +| `--no-canary` 뒤 `--no-before` (`answers/26`) | **6/6** | + +- `RELEASE.md` 3절 예: "1.2.0 카나리 50% → 영향 2/6명, `--no-canary` 한 줄, 끈 뒤 6/6. 전원 배포였다면 6/6명이 영향." +- 풀이 때 "1.2.0 릴리스는 왜 지우지 않나?" → 기록 · 재현 (불변 버전), 원인 분석. + +## 과제 1 · 요청문 예 + +```text +my-work/release-check.json 을 새로 만들어 줘. 웹훅 my-release-check , 본문 release 가 없으면 400. +Golden questions(Code): cases/my-golden.json 의 질문 6개(question · user · expect 낱말)를 항목 6개로, 각 항목에 release. +Ask helpdesk(HTTP POST http://localhost:5678/webhook/my-helpdesk): 항목마다 {question, user, release } 로 (neverError, timeout 30000). +Judge(Code): 답에 expect 낱말 중 하나가 있고 답의 version.release 가 점검한 release 와 같으면 이면 통과. +결과 {release, passed, total, failed, seconds, verdict}: 모두 통과면 ' promote ', 아니면 'rollback'. +기대 결과 cases/lab03-challenge.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `my-release-check` | `cases/lab03-challenge.json`의 `webhook` | +| ② | `release` | 운영이 아니라 후보를 점검하려면 고정 | +| ③ | 같으면 | 고정이 먹었는지 확인 | +| ④ | `promote` | "내보내도 됨" | + +정답: [answers/74-lab03-challenge-release-check.json](answers/74-lab03-challenge-release-check.json) + +| 10/27 결과 (`answers/27`) | | +| --- | --- | +| 1.1.0 | `promote` 6/6 (3.1초) | +| 1.2.0 | `rollback` 0/6 (1.6초) | +| `release` 없음 | 400 | +| `cases/lab03-challenge.json` | **3/3** | +| `call my-release-check '{"release": "2.0.0"}'` | `promote` 6/6 (1.8초) | + +## 과제 2 · 요청문 예 + +```text +my-work/release-check.json 의 Judge 와 Send verdict 사이에 Record gate(HTTP POST http://data-api:8800/ gates/helpdesk ) 를 넣어 줘. +본문: {release, verdict, passed, total, failed, seconds, source: 'my-release-check'} (Judge 의 결과에서). +Record gate 는 timeout 3000, neverError, onError continueRegularOutput (기록이 실패해도 판단은 보냄). +Send verdict 는 Judge 의 결과에 recorded: (기록 응답의 stored 가 true 인지) 를 더해 보낸다. +워크플로우에서 /releases 에는 POST 하지 않는다 . 기대 결과 cases/lab03-challenge2.json, cases/lab03-challenge.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `gates/helpdesk` | AGENTS.md 2절 점검 기록 경로 | +| ② | `continueRegularOutput` | 실패해도 원래 출력으로 계속 (73일차 `Log event`와 같음) | +| ③ | `stored` | `POST /gates/helpdesk`의 응답 `{"stored": true, "count"}` | +| ④ | POST 하지 않는다 | 판단하는 쪽은 실행하지 않음 (AGENTS.md 9절, 최소 권한) | + +정답: [answers/74-lab03-challenge2-gated.json](answers/74-lab03-challenge2-gated.json) (과제 1 정답 + 노드 `Record gate`, `Send verdict`에 `recorded`) + +| 10/27 결과 (`cases/lab03-challenge2.json`) | 고치기 전 (과제 1 정답, `answers/28`) | 고친 뒤 (`answers/29`) | +| --- | --- | --- | +| 1.2.0 점검 · 기록 | ✘ `recorded` 없음 (24.2초, 이때 OpenAI가 느림) | ✔ `rollback` · `recorded` true (10.6초) | +| 1.1.0 점검 · 기록 | ✘ `recorded` 없음 | ✔ `promote` · `recorded` true | +| 1.2.0 배포 거부 (409 기대) | ✔ (기록이 **아예 없어서** 409) | ✔ (마지막 기록 rollback이라 409) | +| 1.1.0 배포 | **멈춤** — `before`가 409 `not gated` · `last_gate: null` | ✔ 200 → 1.1.0 · `stable` | +| 2.0.0 카나리 거부 (409 기대) | (돌지 않음) | ✔ 409, 최유진도 1.1.0 · `stable` | +| 합계 | 멈춤 (보고서 없음) | **5/5** | + +`answers/30` (정책 켜고 손으로): + +| 명령 | 결과 | +| --- | --- | +| `release helpdesk --require-gate on` → `release helpdesk 2.1.0` | `✘ refused: 2.1.0 has no passing gate record (never gated)` | +| `gate cases/golden.json --set release=2.0.0 --max-p95 3` | 6/6 PASS → `gate record → /gates/helpdesk: 2.0.0 promote` | +| `release helpdesk 2.0.0` | 됨 (`active 2.0.0 · deploy needs a passing gate`) | +| `release helpdesk --reset` · `doctor` | 1.0.0 · 정책 꺼짐 · `ready` | + +- 풀이 때: 약한 golden으로 2.0.0을 게이트해도 기록이 남아 배포가 됩니다. **정책은 "기록이 있느냐"만 보고, 기록의 질은 게이트(테스트)의 질입니다.** 운영에서는 어느 테스트 파일로 · 누가 돌린 기록만 인정할지도 정합니다 (`source` · `cases` 칸). diff --git a/Last3Mon/74/LAB-03-Q-DEPLOY-AND-ROLLBACK.md b/Last3Mon/74/LAB-03-Q-DEPLOY-AND-ROLLBACK.md index 3bfb307..ce2fe7c 100644 --- a/Last3Mon/74/LAB-03-Q-DEPLOY-AND-ROLLBACK.md +++ b/Last3Mon/74/LAB-03-Q-DEPLOY-AND-ROLLBACK.md @@ -161,6 +161,24 @@ flowchart LR K --> N(["--no-canary"]) --> R["다시 6명 ✔"] ``` +**이 예제에서는 아무것도 만들지 않습니다.** 예제 2까지 만든 헬프데스크를 그대로 두고, 누가 잘못된 카나리를 켰을 때 **알아채고 끄는 연습**만 합니다. + +**상황.** 운영 릴리스(active)는 1.0.0입니다. 누군가 1.2.0을 50% 카나리로 내보냈습니다. 1.2.0은 지시문 p3("답을 한 문장으로 짧게", [AGENTS.md](AGENTS.md) 2절)를 씁니다. 답이 짧아지면서 해결 방법(프린터 **대기열** 비우기)이 빠집니다. HTTP는 200이라 오류로 보이지 않습니다. **답의 내용을 봐야 알 수 있는 나쁜 릴리스**입니다. + +**명령마다 하는 일.** + +| 순서 | 명령 | 하는 일 | +| --- | --- | --- | +| 1 | `test cases/lab03-watch.json -v` | 테스트 파일 맨 위 `before`가 `reset` 뒤 **카나리 1.2.0 50%를 켭니다** (사고 상황 만들기). 이어서 여섯 직원이 같은 질문 "프린터 출력이 안 돼요"를 보내고, 답에 `대기열`이 있는지 봅니다 | +| 2 | `release helpdesk` | 지금 상태(active · canary)와 카나리 대상 직원을 봅니다. 1번에서 ✘가 난 직원과 맞는지 확인합니다 | +| 3 | `release helpdesk --no-canary` | 카나리를 끕니다. 명령 한 줄 | +| 4 | `test cases/lab03-watch.json --no-before -v` | 같은 테스트를 다시 돌립니다. **`--no-before`가 없으면 `before`가 카나리를 다시 켜서** 끈 효과를 잴 수 없습니다 | +| 5 | `release helpdesk --reset` | 다음 과제를 위해 처음 상태로 | + +**누가 ✘가 나나.** 예제 2의 규칙(bucket < 50이면 카나리)대로 최유진(0) · 정우성(42)만 1.2.0을 받습니다. 1번 출력에서 두 사람의 `version.channel`은 `canary`, `version.release`는 `1.2.0`이고, 나머지 넷은 `stable` · `1.0.0`입니다. 같은 질문을 했는데 **카나리 쪽만** 해결 낱말이 없습니다. 이렇게 운영 쪽과 나란히 비교해 차이를 찾는 것이 "카나리를 본다"의 뜻입니다. + +**끈 뒤.** 카나리가 없으면 여섯 명 모두 active 1.0.0을 받습니다. 4번 출력에서 모두의 `version.channel`이 `stable`인지 봅니다. + **피해 반경(blast radius).** 전원에게 바로 배포했다면 여섯 명 모두 "포털에서 확인하세요"를 받았을 것입니다. 50% 카나리라서 둘만 받았고, 끄는 데는 명령 한 줄입니다. 카나리 비율은 처음에 작게(1~5%) 시작해 늘리는 것이 보통입니다. 오늘은 여섯 명이라 50%로 둘만 걸리게 했습니다. **무엇을 보나.** diff --git a/Last3Mon/74/VIBE-A-TVER.md b/Last3Mon/74/VIBE-A-TVER.md new file mode 100644 index 0000000..62ef44e --- /dev/null +++ b/Last3Mon/74/VIBE-A-TVER.md @@ -0,0 +1,123 @@ +# 7교시 바이브 코딩 · `tver` 정답본 + +> **강사용 정답본** · 학생 저장소에는 수업 뒤에 올립니다. 학생용은 [VIBE-REQ-TVER.md](VIBE-REQ-TVER.md)(요청서) → [VIBE-Q-TVER.md](VIBE-Q-TVER.md). +> 강사가 직접 만든 본: [tver/](tver/README.md) (2026-10-27, Claude Code가 아래 SPEC · IMP-PLAN을 보고 직접 씀, 테스트 11개). CLI 에이전트에 아래 요청문을 보내 재지는 않았습니다. 강사 바이브 코딩 시간은 6절에 수업 때 적습니다. + +## 1. `Doc/TODO.md` 예 + +```markdown +# tver TODO + +## 만들고 싶은 것 +어느 폴더에서나 `tver`로 n8n 워크플로우를 git에 버전으로 남기고, 두 버전을 노드 단위로 비교해 +바뀐 것을 모델 · 지시문 · 흐름으로 나눠 보여 주는 도구. 화면(TUI)도, 다른 프로그램용 결과도. + +## 요청의 말 → 할 일 +| # | 요청의 말 | 할 일 | 꼭 · 나중 | +| --- | --- | --- | --- | +| ① | 노드 단위 비교 | 이름으로 짝지어 더함 · 뺌 · 바뀐 설정 · 연결 | 꼭 | +| ② | 뜻 없는 차이 빼기 | 위치 · id · 순서는 비교에서도 저장에서도 뺌 | 꼭 | +| ③ | 어느 문장이 | 긴 글은 줄 단위 | 꼭 | +| ④ | 셋으로 나눠 한 줄 | 모델 · 지시문 · 흐름 개수 | 꼭 | +| ⑤ | 버전으로, 같으면 저장 X | git 커밋, 요약을 메시지로 | 꼭 | +| ⑥ | 목록 · 두 버전 비교 | log, 두 커밋 비교 | 꼭 | +| ⑦ | 키가 있으면 저장 X | 키 모양을 찾으면 거부, 메시지도 가려서 | 나중 | +| ⑧ | 화면에서 | 목록 · 트리 · 자세히 | 꼭 | +| ⑨ | n8n에서 바로 | 컨테이너에서 내보내 모두 저장 (읽기만) | 나중 | +| ⑩ | 예전 버전을 파일로 | 꺼내기만, 올리는 건 사람 | 나중 | +| ⑪ | 다른 프로그램이 읽게 | JSON, 종료 코드 | 나중 | +| ⑫ | 이름만 바뀐 것 | renamed | 나중 | +| 끝 | 폴더 → 워크플로우만 | nodes · connections가 있는 것만 | 나중 | +| 끝 | n8n을 망가뜨리지 않기 | 저장소는 따로, n8n에 쓰지 않음 | 꼭 | + +## 걱정되는 것 +- 저장할 때도 정리해야 "같다"를 알 수 있다 +- 이름에 공백 · 한글 · `/`가 있는 워크플로우의 파일 이름 +- 키를 거부할 때 메시지에 키가 그대로 나오면 안 됨 +``` + +## 2. `Doc/SPEC.md` 요점 + +VIBE-Q의 요구 표(R1~R14)와 같습니다. 학생 SPEC과 비교할 때 자주 빠지는 것: + +| 자주 빠지는 것 | 왜 필요한가 | +| --- | --- | +| R2를 **저장에도** 적용 (정리한 글로 저장) | 그래야 R5 "같으면 저장 안 함"이 글자 비교로 되고 저장소의 git diff도 읽힘 | +| 분류 규칙을 글로 정해 둠 | 정해 두지 않으면 에이전트가 웹훅 경로를 "모델"로 세기도 함 | +| R10 꺼내기가 저장소를 바꾸지 않음 | 꺼낼 때마다 버전이 생기면 목록이 엉킴 | +| 테스트 저장소는 `tmp_path` | 진짜 `~/.tver/store`가 테스트로 더러워짐 | +| 종료 코드 (diff 0 · 1, 오류 2) | 배포 전 검사(게이트)에서 씀 | + +## 3. `Doc/IMP-PLAN.md` + +[tver/Doc/IMP-PLAN.md](tver/Doc/IMP-PLAN.md) — 엔진 1단계(R1~R4) → 엔진 2단계(R5 · R6) → TUI 1단계(R8) → 설치(R14) → 엔진 3단계(R7 · R11 · R13) → 엔진 4단계(R9 · R10 · R12). + +## 4. 단계별 요청문 예 + +### 엔진 1단계 + +```text +Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 "엔진 1단계"만 만들어 줘. uv 프로젝트, 명령 이름 tver, 코드와 화면 글자는 영어. +- workflow.clean(): 노드에서 id, position, webhookId 와 내보내기 도장(versionId, updatedAt ...)을 빼고 노드를 name 순으로. dumps() 는 키 정렬 JSON. +- diff.compare(old, new): 노드를 name 으로 짝지어 +node, -node, ~set(점 경로 key, before, after), +link, -link. + 80자 넘거나 여러 줄인 값은 difflib 줄 diff 를 lines 에. +- category: Chat model 노드의 parameters.model.value 는 model, systemMessage · text · jsCode 는 prompt, 나머지와 연결은 flow. + summary 한 줄 "N change(s) · model N · prompt N · flow N". +- tver diff a.json b.json. tests: tests/data 의 helpdesk-v1 → v2 가 7개 · model 1 · prompt 1 · flow 5, + 위치 · id 를 바꾸고 노드 순서를 뒤집은 파일은 0개, systemMessage 의 lines 에 "[FAQ-003]" 이 있는 + 줄. +끝나면 IMP-PLAN 체크를 표시하고 확인 명령을 알려 줘. +``` + +### 엔진 2단계 + +```text +엔진 2단계만: Store(repo, 기본 ~/.tver/store, --repo, $TVER_REPO). +- save(파일): dumps 한 글이 저장소의 같은 이름 파일과 같으면 "no change", 다르면 쓰고 git commit. 메시지 "<이름>: " + 바로 전 버전과의 summary 한 줄 (처음이면 new). +- log(이름): 커밋 · 시각 · 메시지. show(rev, 이름): git show rev:이름. tver diff <이름>. +- tests 는 tmp_path 저장소만: 두 번 저장 → 1개, v2 로 바꿔 저장 → 메시지에 "model 1 · prompt 1 · flow 5". +``` + +### TUI 1단계 + +```text +TUI 1단계만: tver <파일> 이면 Textual 화면. 왼쪽 ListView 버전 목록, 오른쪽 Tree(노드 → 바뀐 것, 고른 버전 vs 바로 전 버전, worker 로 비교), +트리의 잎을 고르면 아래 Static 에 줄 diff 또는 전 · 후. [s] 지금 파일을 버전으로 저장하고 목록을 다시, [q] 끝. +엔진(diff · store)은 고치지 말고 불러 쓰기만. pytest 로 [s] 뒤 목록이 하나 늘고 트리 제목에 요약이 나오는지. +``` + +## 5. 결과 확인 (강사 본, 2026-10-27) + +| 요구 | 확인 | 결과 | +| --- | --- | --- | +| R1 · R3 · R4 | `tver diff reference/helpdesk-v1.json reference/helpdesk-v2.json` | 7줄 (`flow.py diff`와 같음), systemMessage 줄 diff, `model 1 · prompt 1 · flow 5` | +| R2 | 위치 · id · 순서만 바꾼 파일 (pytest) | 0개 | +| R5 · R6 | 같은 파일 두 번 save → `log` | 1개, 바꿔 저장 → 2개 | +| R7 | 가짜 키(테스트 안에서 만듦)를 넣고 save | 거부, 메시지에 원래 값 없음 | +| R8 | pytest `run_test` | `[s]` 뒤 목록 2개, 트리 제목 요약 | +| R9 | `tver pull` (강사 n8n) | 192 new version(s) | +| R10 | `restore` → 원본과 `diff` | 0 change(s), 버전 수 그대로 | +| R11 | `diff a a` · `a b` · 없는 파일 | 0 · 1 · 2 | +| R12 | `Search FAQ` → `Find FAQ` (pytest) | `renamed` 1개 | +| R13 | `tver save answers` | 5개 저장, golden 테스트 파일 skipped | +| R14 | `uv tool install --editable .`, `/tmp`에서 `tver --help` | 됨 | + +오늘 Lab의 helpdesk 정답 여섯 단계를 차례로 저장했을 때의 `log` (위가 최신): + +```text +87090f0 helpdesk.json: 1 change(s) · model 0 · prompt 1 · flow 0 ← Lab 03 예제 2 (카나리: Pick release의 jsCode) +9e2ac9a helpdesk.json: 14 change(s) · model 0 · prompt 2 · flow 12 ← Lab 02 과제 2 (응답 코드 · 400) +e0dc13e helpdesk.json: 15 change(s) · model 1 · prompt 1 · flow 13 ← Lab 02 예제 1 (릴리스를 밖에서) +b8461aa helpdesk.json: 12 change(s) · model 0 · prompt 2 · flow 10 ← Lab 01 과제 2 (기본 지시문) +76c5e18 helpdesk.json: 6 change(s) · model 0 · prompt 2 · flow 4 ← Lab 01 과제 1 (지시문 저장소) +7c337b5 helpdesk.json: new ← Lab 01 예제 2 +``` + +- 풀이 거리: 카나리(87090f0)는 `Pick release`의 **Code** 한 칸이라 "prompt 1"로 셉니다. 규칙상 `jsCode`를 지시문으로 셌기 때문입니다. 코드와 지시문을 나눠 세고 싶으면 `code` 분류를 하나 더 두면 됩니다 — 분류 규칙은 **팀이 정하는 것**이라는 예. + +## 6. 강사 바이브 코딩 (8교시, 수업 때 적음) + +| 주제 | 요청문 요지 | 걸린 시간 · 결과 | +| --- | --- | --- | +| 게이트 붙이기 | "tver save 전에 지정한 테스트 파일로 flow.py gate 를 돌려 FAIL 이면 저장 안 함" | | +| 지시문만 따로 | "systemMessage 를 꺼내 prompts/<노드>.md 로 저장, 지시문 diff 만 보기" | | +| 코드 분류 | "jsCode 는 prompt 가 아니라 code 로 따로 세기" | | diff --git a/Last3Mon/74/answers/00-prep.txt b/Last3Mon/74/answers/00-prep.txt new file mode 100644 index 0000000..4e48c19 --- /dev/null +++ b/Last3Mon/74/answers/00-prep.txt @@ -0,0 +1,26 @@ +$ uv run tools/flow.py release helpdesk --reset +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate promote 6/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate rollback 0/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +$ uv run tools/flow.py push reference/helpdesk-v1.json reference/helpdesk-v2.json +✔ pushed '74 IT helpdesk v1 (provided)' (id w0ed73a832099852) + POST http://localhost:5678/webhook/rel-v1 +✔ pushed '74 IT helpdesk v2 (provided)' (id w8e65b72e125f790) + POST http://localhost:5678/webhook/rel-v2 + +$ uv run tools/flow.py doctor +! n8n install folder /Users/mcchae/day65-n8n + → day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더> +✔ n8n container day65-n8n-n8n-1 +✔ n8n http://localhost:5678 +✔ data API http://localhost:8800 +✔ releases /releases/helpdesk · gates /gates/helpdesk (day 74 data API) +✔ n8n → http://data-api:8800 +✔ credential 'OpenAI (.env)' (openAiCourse0001) +✔ node catalog (for check) +✔ CLI coding agent: codex, claude, agy +ready diff --git a/Last3Mon/74/answers/01-lab01-diff.txt b/Last3Mon/74/answers/01-lab01-diff.txt new file mode 100644 index 0000000..5e2578e --- /dev/null +++ b/Last3Mon/74/answers/01-lab01-diff.txt @@ -0,0 +1,24 @@ +$ uv run tools/flow.py diff reference/helpdesk-v1.json reference/helpdesk-v2.json +74 IT helpdesk v1 (provided) → 74 IT helpdesk v2 (provided) ++ node 'Review check' (code) +~ 'Answer' parameters.options.systemMessage: changed (307 → 419 chars) +~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano +~ 'Question webhook' parameters.path: rel-v1 → rel-v2 ++ link Answer → Review check (main) ++ link Review check → Build result (main) +- link Answer → Build result (main) +7 change(s) + +$ uv run tools/flow.py diff reference/helpdesk-v1.json reference/helpdesk-v2.json --full +74 IT helpdesk v1 (provided) → 74 IT helpdesk v2 (provided) ++ node 'Review check' (code) +~ 'Answer' parameters.options.systemMessage: changed (307 → 419 chars) + before: You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say "찾지 못했습니다" and that a staff member will help. Use 합니다체. + after: You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say "찾지 못했습니다" and that a staff member will help. Use 합니다체. +When the answer comes from a FAQ entry, end with its id in brackets, for example [FAQ-003]. Keep answers short. +~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano +~ 'Question webhook' parameters.path: rel-v1 → rel-v2 ++ link Answer → Review check (main) ++ link Review check → Build result (main) +- link Answer → Build result (main) +7 change(s) diff --git a/Last3Mon/74/answers/02-lab01-call-v1-v2.txt b/Last3Mon/74/answers/02-lab01-call-v1-v2.txt new file mode 100644 index 0000000..acf8186 --- /dev/null +++ b/Last3Mon/74/answers/02-lab01-call-v1-v2.txt @@ -0,0 +1,31 @@ +$ uv run tools/flow.py call rel-v1 requests/faq-vpn.json -v +HTTP 200 · 2.8s + → request POST /webhook/rel-v1 + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + +$ uv run tools/flow.py call rel-v2 requests/faq-vpn.json -v +HTTP 200 · 1.0s + → request POST /webhook/rel-v2 + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } diff --git a/Last3Mon/74/answers/03-lab01-version.json b/Last3Mon/74/answers/03-lab01-version.json new file mode 100644 index 0000000..e8bf6c6 --- /dev/null +++ b/Last3Mon/74/answers/03-lab01-version.json @@ -0,0 +1,72 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 3, + "results": [ + { + "case": "버전이 결과에", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.5, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } + }, + { + "case": "접수 현황도 버전과 함께", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } + }, + { + "case": "빈 질문", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/03-lab01-version.txt b/Last3Mon/74/answers/03-lab01-version.txt new file mode 100644 index 0000000..50e13d8 --- /dev/null +++ b/Last3Mon/74/answers/03-lab01-version.txt @@ -0,0 +1,55 @@ +$ uv run tools/flow.py push reference/lab01-example2-version.json +! unpublished '74 IT helpdesk + release API (reference)' (w2caac7c1a6981b8): it used the same webhook path +✔ pushed '74 IT helpdesk + version (reference)' (id w473a152b356a298) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab01.json -v +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 버전이 결과에 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 접수 현황도 버전과 함께 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/3 +report: runs/lab01-20261027-183702.json diff --git a/Last3Mon/74/answers/04-lab01-compare-v1.json b/Last3Mon/74/answers/04-lab01-compare-v1.json new file mode 100644 index 0000000..7ae1c87 --- /dev/null +++ b/Last3Mon/74/answers/04-lab01-compare-v1.json @@ -0,0 +1,314 @@ +{ + "cases": "lab01-compare.json", + "webhook": "rel-v1", + "passed": 9, + "total": 15, + "results": [ + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v1", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v1", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v1", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v1", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-003]']" + ], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v1", + "run": 2, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-003]']" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v1", + "run": 3, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-003]']" + ], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 내역은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v1", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v1", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v1", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v1", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-']" + ], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v1", + "run": 2, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-']" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v1", + "run": 3, + "passed": false, + "problems": [ + "answer has none of ['[FAQ-']" + ], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v1", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v1", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v1", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [] + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/04-lab01-compare-v1.txt b/Last3Mon/74/answers/04-lab01-compare-v1.txt new file mode 100644 index 0000000..fd32970 --- /dev/null +++ b/Last3Mon/74/answers/04-lab01-compare-v1.txt @@ -0,0 +1,19 @@ +$ uv run tools/flow.py test cases/lab01-compare.json --webhook rel-v1 --repeat 3 +POST http://localhost:5678/webhook/rel-v1 · 5 case(s) × 3 +✔ VPN · 해결 낱말 #1 · 1.2s +✔ VPN · 해결 낱말 #2 · 1.2s +✔ VPN · 해결 낱말 #3 · 1.1s +✘ VPN · 출처 번호 #1 · 1.1s · answer has none of ['[FAQ-003]'] +✘ VPN · 출처 번호 #2 · 1.2s · answer has none of ['[FAQ-003]'] +✘ VPN · 출처 번호 #3 · 1.3s · answer has none of ['[FAQ-003]'] +✔ 프린터 · 해결 낱말 #1 · 1.0s +✔ 프린터 · 해결 낱말 #2 · 1.1s +✔ 프린터 · 해결 낱말 #3 · 1.1s +✘ 프린터 · 출처 번호 #1 · 1.1s · answer has none of ['[FAQ-'] +✘ 프린터 · 출처 번호 #2 · 1.2s · answer has none of ['[FAQ-'] +✘ 프린터 · 출처 번호 #3 · 1.0s · answer has none of ['[FAQ-'] +✔ 접수 현황 #1 · 0.9s +✔ 접수 현황 #2 · 1.0s +✔ 접수 현황 #3 · 1.1s +Passed 9/15 +report: runs/lab01-compare-20261027-183719.json diff --git a/Last3Mon/74/answers/05-lab01-compare-v2.json b/Last3Mon/74/answers/05-lab01-compare-v2.json new file mode 100644 index 0000000..9a2f8f0 --- /dev/null +++ b/Last3Mon/74/answers/05-lab01-compare-v2.json @@ -0,0 +1,302 @@ +{ + "cases": "lab01-compare.json", + "webhook": "rel-v2", + "passed": 15, + "total": 15, + "results": [ + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v2", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v2", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 해결 낱말", + "webhook": "rel-v2", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v2", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v2", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN · 출처 번호", + "webhook": "rel-v2", + "run": 3, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v2", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v2", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 해결 낱말", + "webhook": "rel-v2", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v2", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v2", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "프린터 · 출처 번호", + "webhook": "rel-v2", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v2", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 “처리 중”입니다. 메모에 따르면 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v2", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 “처리 중”입니다. 메모에 따르면 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [] + } + }, + { + "case": "접수 현황", + "webhook": "rel-v2", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 “처리 중”입니다. 인증서 발급 승인 대기 중이며 “10월 20일 완료 예정”이라고 기록되어 있습니다.", + "sources": [] + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/05-lab01-compare-v2.txt b/Last3Mon/74/answers/05-lab01-compare-v2.txt new file mode 100644 index 0000000..5881d69 --- /dev/null +++ b/Last3Mon/74/answers/05-lab01-compare-v2.txt @@ -0,0 +1,19 @@ +$ uv run tools/flow.py test cases/lab01-compare.json --webhook rel-v2 --repeat 3 +POST http://localhost:5678/webhook/rel-v2 · 5 case(s) × 3 +✔ VPN · 해결 낱말 #1 · 1.0s +✔ VPN · 해결 낱말 #2 · 1.0s +✔ VPN · 해결 낱말 #3 · 1.0s +✔ VPN · 출처 번호 #1 · 0.9s +✔ VPN · 출처 번호 #2 · 1.1s +✔ VPN · 출처 번호 #3 · 0.9s +✔ 프린터 · 해결 낱말 #1 · 1.4s +✔ 프린터 · 해결 낱말 #2 · 1.1s +✔ 프린터 · 해결 낱말 #3 · 1.2s +✔ 프린터 · 출처 번호 #1 · 1.4s +✔ 프린터 · 출처 번호 #2 · 1.1s +✔ 프린터 · 출처 번호 #3 · 1.2s +✔ 접수 현황 #1 · 1.1s +✔ 접수 현황 #2 · 1.0s +✔ 접수 현황 #3 · 1.0s +Passed 15/15 +report: runs/lab01-compare-20261027-183736.json diff --git a/Last3Mon/74/answers/06-lab01-bench-v1-v2.txt b/Last3Mon/74/answers/06-lab01-bench-v1-v2.txt new file mode 100644 index 0000000..62643aa --- /dev/null +++ b/Last3Mon/74/answers/06-lab01-bench-v1-v2.txt @@ -0,0 +1,19 @@ +$ uv run tools/flow.py bench rel-v1 requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/rel-v1 × 5 +✔ #1 · HTTP 200 · 1.26s · route=자동 답변 +✔ #2 · HTTP 200 · 1.11s · route=자동 답변 +✔ #3 · HTTP 200 · 1.23s · route=자동 답변 +✔ #4 · HTTP 200 · 1.34s · route=자동 답변 +✔ #5 · HTTP 200 · 1.18s · route=자동 답변 +p50 1.23s · p95 1.34s · max 1.34s · failures 0/5 +report: runs/bench-rel-v1-20261027-183742.json + +$ uv run tools/flow.py bench rel-v2 requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/rel-v2 × 5 +✔ #1 · HTTP 200 · 1.20s · route=자동 답변 +✔ #2 · HTTP 200 · 0.97s · route=자동 답변 +✔ #3 · HTTP 200 · 0.98s · route=자동 답변 +✔ #4 · HTTP 200 · 1.02s · route=자동 답변 +✔ #5 · HTTP 200 · 0.98s · route=자동 답변 +p50 0.98s · p95 1.20s · max 1.20s · failures 0/5 +report: runs/bench-rel-v2-20261027-183747.json diff --git a/Last3Mon/74/answers/07-lab01-challenge-before.json b/Last3Mon/74/answers/07-lab01-challenge-before.json new file mode 100644 index 0000000..d766e11 --- /dev/null +++ b/Last3Mon/74/answers/07-lab01-challenge-before.json @@ -0,0 +1,83 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 2, + "total": 3, + "results": [ + { + "case": "지시문은 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.prompt_source = null (expected registry)" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } + }, + { + "case": "p1은 출처 번호를 붙이지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/07-lab01-challenge-before.txt b/Last3Mon/74/answers/07-lab01-challenge-before.txt new file mode 100644 index 0000000..d5eda23 --- /dev/null +++ b/Last3Mon/74/answers/07-lab01-challenge-before.txt @@ -0,0 +1,59 @@ +$ uv run tools/flow.py test cases/lab01-challenge.json -v +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✘ 지시문은 저장소에서 · 1.2s · version.prompt_source = null (expected registry) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 접수 현황 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +Passed 2/3 +report: runs/lab01-challenge-20261027-183751.json diff --git a/Last3Mon/74/answers/08-lab01-challenge.json b/Last3Mon/74/answers/08-lab01-challenge.json new file mode 100644 index 0000000..663fe59 --- /dev/null +++ b/Last3Mon/74/answers/08-lab01-challenge.json @@ -0,0 +1,84 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 3, + "results": [ + { + "case": "지시문은 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "p1은 출처 번호를 붙이지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/08-lab01-challenge.txt b/Last3Mon/74/answers/08-lab01-challenge.txt new file mode 100644 index 0000000..99f5cdb --- /dev/null +++ b/Last3Mon/74/answers/08-lab01-challenge.txt @@ -0,0 +1,67 @@ +$ uv run tools/flow.py push answers/74-lab01-challenge-registry.json +! unpublished '74 IT helpdesk + version (reference)' (w473a152b356a298): it used the same webhook path +✔ pushed '74 IT helpdesk + prompt registry (answer)' (id w10c91b5c7439d37) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab01-challenge.json -v +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 3/3 +report: runs/lab01-challenge-20261027-183810.json diff --git a/Last3Mon/74/answers/09-lab01-after-challenge.json b/Last3Mon/74/answers/09-lab01-after-challenge.json new file mode 100644 index 0000000..0ef1756 --- /dev/null +++ b/Last3Mon/74/answers/09-lab01-after-challenge.json @@ -0,0 +1,74 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 3, + "results": [ + { + "case": "버전이 결과에", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 현황도 버전과 함께", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "빈 질문", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/09-lab01-after-challenge.txt b/Last3Mon/74/answers/09-lab01-after-challenge.txt new file mode 100644 index 0000000..6838dd1 --- /dev/null +++ b/Last3Mon/74/answers/09-lab01-after-challenge.txt @@ -0,0 +1,52 @@ +$ uv run tools/flow.py test cases/lab01.json -v +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 버전이 결과에 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황도 버전과 함께 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/3 +report: runs/lab01-20261027-183812.json diff --git a/Last3Mon/74/answers/10-lab01-challenge2-before.json b/Last3Mon/74/answers/10-lab01-challenge2-before.json new file mode 100644 index 0000000..9af6473 --- /dev/null +++ b/Last3Mon/74/answers/10-lab01-challenge2-before.json @@ -0,0 +1,110 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 2, + "total": 4, + "results": [ + { + "case": "평소 · 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "저장소 고장 · 기본 지시문으로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 500 (expected 200)", + "route = null (expected 자동 답변)", + "version.prompt_source = null (expected fallback)", + "answer has none of ['인증서']" + ], + "seconds": 0.1, + "status": 500, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "message": "Error in workflow" + } + }, + { + "case": "저장소 느림 · 오래 기다리지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "took 9.1s (expected at most 6s)", + "version.prompt_source = registry (expected fallback)" + ], + "seconds": 9.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "되돌린 뒤 · 다시 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/10-lab01-challenge2-before.txt b/Last3Mon/74/answers/10-lab01-challenge2-before.txt new file mode 100644 index 0000000..7fed6f8 --- /dev/null +++ b/Last3Mon/74/answers/10-lab01-challenge2-before.txt @@ -0,0 +1,137 @@ +$ uv run tools/flow.py test cases/lab01-challenge2.json -v +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✘ 저장소 고장 · 기본 지시문으로 답 · 0.1s · status 500 (expected 200); route = null (expected 자동 답변); version.prompt_source = null (expected fallback); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "slow", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "slow", + "slow_seconds": 8.0 + } +✘ 저장소 느림 · 오래 기다리지 않음 · 9.1s · took 9.1s (expected at most 6s); version.prompt_source = registry (expected fallback) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 되돌린 뒤 · 다시 저장소에서 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 2/4 +report: runs/lab01-challenge2-20261027-183824.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } diff --git a/Last3Mon/74/answers/11-lab01-challenge2.json b/Last3Mon/74/answers/11-lab01-challenge2.json new file mode 100644 index 0000000..4239fed --- /dev/null +++ b/Last3Mon/74/answers/11-lab01-challenge2.json @@ -0,0 +1,112 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소 · 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "저장소 고장 · 기본 지시문으로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } + }, + { + "case": "저장소 느림 · 오래 기다리지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } + }, + { + "case": "되돌린 뒤 · 다시 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/11-lab01-challenge2.txt b/Last3Mon/74/answers/11-lab01-challenge2.txt new file mode 100644 index 0000000..bedd2be --- /dev/null +++ b/Last3Mon/74/answers/11-lab01-challenge2.txt @@ -0,0 +1,152 @@ +$ uv run tools/flow.py push answers/74-lab01-challenge2-fallback.json +! unpublished '74 IT helpdesk + prompt registry (answer)' (w10c91b5c7439d37): it used the same webhook path +✔ pushed '74 IT helpdesk + prompt fallback (answer)' (id w5215c7f75644ed0) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab01-challenge2.json -v +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 기본 지시문으로 답 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "slow", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "slow", + "slow_seconds": 8.0 + } +✔ 저장소 느림 · 오래 기다리지 않음 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 되돌린 뒤 · 다시 저장소에서 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 4/4 +report: runs/lab01-challenge2-20261027-183845.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } diff --git a/Last3Mon/74/answers/12-lab01-challenge-after2.json b/Last3Mon/74/answers/12-lab01-challenge-after2.json new file mode 100644 index 0000000..a085ee0 --- /dev/null +++ b/Last3Mon/74/answers/12-lab01-challenge-after2.json @@ -0,0 +1,84 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 3, + "results": [ + { + "case": "지시문은 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "p1은 출처 번호를 붙이지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/12-lab01-challenge-after2.txt b/Last3Mon/74/answers/12-lab01-challenge-after2.txt new file mode 100644 index 0000000..7514b00 --- /dev/null +++ b/Last3Mon/74/answers/12-lab01-challenge-after2.txt @@ -0,0 +1,62 @@ +$ uv run tools/flow.py test cases/lab01-challenge.json -v +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 3/3 +report: runs/lab01-challenge-20261027-183849.json diff --git a/Last3Mon/74/answers/13-lab02-before.json b/Last3Mon/74/answers/13-lab02-before.json new file mode 100644 index 0000000..eebe884 --- /dev/null +++ b/Last3Mon/74/answers/13-lab02-before.json @@ -0,0 +1,144 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 1, + "total": 5, + "results": [ + { + "case": "운영 중인 릴리스 (active)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.channel = null (expected stable)" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "후보 시험 · 1.1.0 고정", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.release = 1.0.0 (expected 1.1.0)", + "version.channel = null (expected pinned)", + "version.prompt_version = p1 (expected p2)", + "answer has none of ['[FAQ-003]']" + ], + "seconds": 1.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "후보 시험 · 2.0.0 (작은 모델)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.release = 1.0.0 (expected 2.0.0)", + "version.model = gpt-4.1-mini (expected gpt-5.4-nano)" + ], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "없는 릴리스", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 200 (expected 400)", + "known has none of ['1.0.0']" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } + }, + { + "case": "빈 질문", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/13-lab02-before.txt b/Last3Mon/74/answers/13-lab02-before.txt new file mode 100644 index 0000000..8fb71a6 --- /dev/null +++ b/Last3Mon/74/answers/13-lab02-before.txt @@ -0,0 +1,142 @@ +$ uv run tools/flow.py release helpdesk +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate promote 6/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate rollback 0/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +$ uv run tools/flow.py test cases/lab02.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ 운영 중인 릴리스 (active) · 1.2s · version.channel = null (expected stable) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 후보 시험 · 1.1.0 고정 · 1.6s · version.release = 1.0.0 (expected 1.1.0); version.channel = null (expected pinned); version.prompt_version = p1 (expected p2); answer has none of ['[FAQ-003]'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 후보 시험 · 2.0.0 (작은 모델) · 1.3s · version.release = 1.0.0 (expected 2.0.0); version.model = gpt-4.1-mini (expected gpt-5.4-nano) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 없는 릴리스 · 1.2s · status 200 (expected 400); known has none of ['1.0.0'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/5 +report: runs/lab02-20261027-183854.json diff --git a/Last3Mon/74/answers/14-lab02-release.json b/Last3Mon/74/answers/14-lab02-release.json new file mode 100644 index 0000000..53cd689 --- /dev/null +++ b/Last3Mon/74/answers/14-lab02-release.json @@ -0,0 +1,132 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 5, + "total": 5, + "results": [ + { + "case": "운영 중인 릴리스 (active)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "후보 시험 · 1.1.0 고정", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "후보 시험 · 2.0.0 (작은 모델)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 찾지 못했습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "없는 릴리스", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 400, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + }, + "response": { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } + }, + { + "case": "빈 질문", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.2, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/14-lab02-release.txt b/Last3Mon/74/answers/14-lab02-release.txt new file mode 100644 index 0000000..305d85b --- /dev/null +++ b/Last3Mon/74/answers/14-lab02-release.txt @@ -0,0 +1,140 @@ +$ uv run tools/flow.py push reference/lab02-example1-release.json +! unpublished '74 IT helpdesk + prompt fallback (answer)' (w5215c7f75644ed0): it used the same webhook path +✔ pushed '74 IT helpdesk + release API (reference)' (id w2caac7c1a6981b8) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab02.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 후보 시험 · 1.1.0 고정 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 찾지 못했습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 없는 릴리스 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ 빈 질문 · 0.2s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 5/5 +report: runs/lab02-20261027-183911.json diff --git a/Last3Mon/74/answers/15-lab02-lab01-again.json b/Last3Mon/74/answers/15-lab02-lab01-again.json new file mode 100644 index 0000000..fe340d9 --- /dev/null +++ b/Last3Mon/74/answers/15-lab02-lab01-again.json @@ -0,0 +1,116 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소 · 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "저장소 고장 · 기본 지시문으로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "fallback" + } + } + }, + { + "case": "저장소 느림 · 오래 기다리지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "fallback" + } + } + }, + { + "case": "되돌린 뒤 · 다시 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/15-lab02-lab01-again.txt b/Last3Mon/74/answers/15-lab02-lab01-again.txt new file mode 100644 index 0000000..df91e91 --- /dev/null +++ b/Last3Mon/74/answers/15-lab02-lab01-again.txt @@ -0,0 +1,21 @@ +$ uv run tools/flow.py test cases/lab01-challenge.json +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 1.2s +✔ p1은 출처 번호를 붙이지 않음 · 1.3s +✔ 접수 현황 · 1.2s +Passed 3/3 +report: runs/lab01-challenge-20261027-183915.json + +$ uv run tools/flow.py test cases/lab01-challenge2.json +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.4s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 기본 지시문으로 답 · 1.1s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} +✔ 저장소 느림 · 오래 기다리지 않음 · 3.1s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 되돌린 뒤 · 다시 저장소에서 · 1.3s +Passed 4/4 +report: runs/lab01-challenge2-20261027-183922.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/74/answers/16-lab02-golden.json b/Last3Mon/74/answers/16-lab02-golden.json new file mode 100644 index 0000000..9a9db6c --- /dev/null +++ b/Last3Mon/74/answers/16-lab02-golden.json @@ -0,0 +1,172 @@ +{ + "cases": "golden.json", + "webhook": "my-helpdesk", + "passed": 6, + "total": 6, + "results": [ + { + "case": "VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 찾지 못했습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정합니다. [FAQ-001] \n접수 건은 찾지 못했습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보해 주시기 바랍니다. [FAQ-005] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 **처리 중**입니다. 메모에 따르면 **인증서 발급 승인 대기 중이며 10월 20일 완료 예정**입니다.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1001은 **처리 완료** 상태이며, **새 배터리로 교체했습니다**. 추가로 확인이 필요하시면 담당자에게 문의해 주세요.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/16-lab02-golden.txt b/Last3Mon/74/answers/16-lab02-golden.txt new file mode 100644 index 0000000..d569a9a --- /dev/null +++ b/Last3Mon/74/answers/16-lab02-golden.txt @@ -0,0 +1,75 @@ +$ uv run tools/flow.py gate cases/golden.json --set release=1.0.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.2s +✔ 비밀번호 · 1.2s +✔ 프린터 · 1.1s +✔ 메일함 · 1.3s +✔ 접수 처리 중 · 1.0s +✔ 접수 처리 완료 · 1.6s +Passed 6/6 +report: runs/golden-20261027-183929.json +gate [release=1.0.0]: pass 6/6 (100%) · p95 1.6s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote + +$ uv run tools/flow.py gate cases/golden.json --set release=1.1.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.0s +✔ 프린터 · 1.1s +✔ 메일함 · 1.1s +✔ 접수 처리 중 · 1.1s +✔ 접수 처리 완료 · 1.1s +Passed 6/6 +report: runs/golden-20261027-183936.json +gate [release=1.1.0]: pass 6/6 (100%) · p95 1.1s → PASS +✔ gate record → /gates/helpdesk: 1.1.0 promote + +$ uv run tools/flow.py gate cases/golden.json --set release=1.2.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 0.8s +✔ 비밀번호 · 0.9s +✔ 프린터 · 0.8s +✔ 메일함 · 1.0s +✔ 접수 처리 중 · 1.2s +✔ 접수 처리 완료 · 1.5s +Passed 6/6 +report: runs/golden-20261027-183942.json +gate [release=1.2.0]: pass 6/6 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 1.2.0 promote + +$ uv run tools/flow.py gate cases/golden.json --set release=2.0.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.1s +✔ 프린터 · 2.0s +✔ 메일함 · 1.2s +✔ 접수 처리 중 · 1.0s +✔ 접수 처리 완료 · 1.2s +Passed 6/6 +report: runs/golden-20261027-183950.json +gate [release=2.0.0]: pass 6/6 (100%) · p95 2.0s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote + +$ uv run tools/flow.py call my-helpdesk '{"question": "프린터 출력이 안 돼요", "user": "최유진", "release": "1.2.0"}' -v +HTTP 200 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.2.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } diff --git a/Last3Mon/74/answers/17-lab02-baseline.json b/Last3Mon/74/answers/17-lab02-baseline.json new file mode 100644 index 0000000..6c2061b --- /dev/null +++ b/Last3Mon/74/answers/17-lab02-baseline.json @@ -0,0 +1,500 @@ +{ + "cases": "golden.json", + "webhook": "my-helpdesk", + "passed": 18, + "total": 18, + "results": [ + { + "case": "VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "VPN", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "VPN", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 현재 접수된 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 현재 접수된 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.5, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체되었습니다. 추가 문의 사항이 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체되었습니다. 추가 문의 사항이 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체되었습니다. 추가 문의 사항이 있으시면 연락 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/17-lab02-baseline.txt b/Last3Mon/74/answers/17-lab02-baseline.txt new file mode 100644 index 0000000..c458dae --- /dev/null +++ b/Last3Mon/74/answers/17-lab02-baseline.txt @@ -0,0 +1,44 @@ +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 --set release=1.0.0 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.18s · route=자동 답변, release=1.0.0 +✔ #2 · HTTP 200 · 1.42s · route=자동 답변, release=1.0.0 +✔ #3 · HTTP 200 · 1.17s · route=자동 답변, release=1.0.0 +✔ #4 · HTTP 200 · 1.23s · route=자동 답변, release=1.0.0 +✔ #5 · HTTP 200 · 1.23s · route=자동 답변, release=1.0.0 +p50 1.23s · p95 1.42s · max 1.42s · failures 0/5 +report: runs/bench-my-helpdesk-20261027-183957.json + +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 --set release=2.0.0 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.10s · route=자동 답변, release=2.0.0 +✔ #2 · HTTP 200 · 1.24s · route=자동 답변, release=2.0.0 +✔ #3 · HTTP 200 · 1.05s · route=자동 답변, release=2.0.0 +✔ #4 · HTTP 200 · 1.12s · route=자동 답변, release=2.0.0 +✔ #5 · HTTP 200 · 1.12s · route=자동 답변, release=2.0.0 +p50 1.12s · p95 1.24s · max 1.24s · failures 0/5 +report: runs/bench-my-helpdesk-20261027-184003.json + +$ uv run tools/flow.py gate cases/golden.json --set release=1.0.0 --repeat 3 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 3 +✔ VPN #1 · 1.1s +✔ VPN #2 · 1.1s +✔ VPN #3 · 1.2s +✔ 비밀번호 #1 · 1.1s +✔ 비밀번호 #2 · 1.3s +✔ 비밀번호 #3 · 1.3s +✔ 프린터 #1 · 1.1s +✔ 프린터 #2 · 1.0s +✔ 프린터 #3 · 1.3s +✔ 메일함 #1 · 1.1s +✔ 메일함 #2 · 1.1s +✔ 메일함 #3 · 1.1s +✔ 접수 처리 중 #1 · 1.1s +✔ 접수 처리 중 #2 · 1.0s +✔ 접수 처리 중 #3 · 1.0s +✔ 접수 처리 완료 #1 · 1.5s +✔ 접수 처리 완료 #2 · 1.1s +✔ 접수 처리 완료 #3 · 1.1s +Passed 18/18 +report: runs/golden-20261027-184024.json +gate [release=1.0.0]: pass 18/18 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote diff --git a/Last3Mon/74/answers/18-lab02-my-golden.json b/Last3Mon/74/answers/18-lab02-my-golden.json new file mode 100644 index 0000000..376752a --- /dev/null +++ b/Last3Mon/74/answers/18-lab02-my-golden.json @@ -0,0 +1,254 @@ +{ + "cases": "74-lab02-challenge-golden.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "VPN · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003] \n접수 건은 찾지 못했습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "VPN · 순서도 (업데이트)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정합니다. [FAQ-001] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보하시면 됩니다. [FAQ-005] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중 · 상태와 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 **처리 중**입니다. 메모에 따르면 **인증서 발급 승인 대기 중이며 10월 20일 완료 예정**입니다.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중 · 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 **처리 중**입니다. 메모에 따르면 **인증서 발급 승인 대기 중이며 10월 20일 완료 예정**입니다.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1001은 **처리 완료** 상태이며, **새 배터리로 교체했습니다**. 추가로 확인이 필요하시면 담당자에게 문의하시기 바랍니다.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "포털로 떠넘기지 않기", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/18-lab02-my-golden.txt b/Last3Mon/74/answers/18-lab02-my-golden.txt new file mode 100644 index 0000000..6c10f0e --- /dev/null +++ b/Last3Mon/74/answers/18-lab02-my-golden.txt @@ -0,0 +1,63 @@ +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.0.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.2s +✔ VPN · 순서도 (업데이트) · 1.3s +✔ 비밀번호 · 해결 방법 · 1.4s +✔ 프린터 · 해결 방법 · 1.1s +✔ 메일함 · 해결 방법 · 1.2s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.1s +✔ 포털로 떠넘기지 않기 · 1.1s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184034.json +gate [release=1.0.0]: pass 9/9 (100%) · p95 1.4s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote + +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.1.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.0s +✔ VPN · 순서도 (업데이트) · 1.7s +✔ 비밀번호 · 해결 방법 · 1.1s +✔ 프린터 · 해결 방법 · 1.3s +✔ 메일함 · 해결 방법 · 1.0s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.0s +✔ 포털로 떠넘기지 않기 · 1.2s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184045.json +gate [release=1.1.0]: pass 9/9 (100%) · p95 1.7s → PASS +✔ gate record → /gates/helpdesk: 1.1.0 promote + +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.2.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✘ VPN · 해결 방법 · 0.9s · answer has none of ['인증서'] +✘ VPN · 순서도 (업데이트) · 0.7s · answer has none of ['업데이트', '최신'] +✘ 비밀번호 · 해결 방법 · 0.9s · answer has none of ['비밀번호 찾기', '휴대폰'] +✘ 프린터 · 해결 방법 · 0.9s · answer has none of ['대기열', '전원'] +✘ 메일함 · 해결 방법 · 0.9s · answer has none of ['보관함', '삭제'] +✘ 접수 처리 중 · 상태와 예정일 · 1.0s · answer has none of ['처리 중'] +✘ 접수 처리 중 · 예정일 · 0.9s · answer has none of ['10월 20일', '20일'] +✘ 접수 처리 완료 · 1.3s · answer has none of ['처리 완료', '완료'] +✔ 포털로 떠넘기지 않기 · 0.7s +Passed 1/9 +report: runs/74-lab02-challenge-golden-20261027-184053.json +gate [release=1.2.0]: pass 1/9 (11%) · p95 1.3s → FAIL · pass rate 11% < 100% +✔ gate record → /gates/helpdesk: 1.2.0 rollback + +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.0.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.1s +✔ VPN · 순서도 (업데이트) · 1.3s +✔ 비밀번호 · 해결 방법 · 1.3s +✔ 프린터 · 해결 방법 · 1.3s +✔ 메일함 · 해결 방법 · 1.3s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.1s +✔ 접수 처리 완료 · 1.2s +✔ 포털로 떠넘기지 않기 · 1.1s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184104.json +gate [release=2.0.0]: pass 9/9 (100%) · p95 1.3s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote diff --git a/Last3Mon/74/answers/19-lab02-challenge2-before.json b/Last3Mon/74/answers/19-lab02-challenge2-before.json new file mode 100644 index 0000000..a5bd3cd --- /dev/null +++ b/Last3Mon/74/answers/19-lab02-challenge2-before.json @@ -0,0 +1,254 @@ +{ + "cases": "74-lab02-challenge-golden.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "VPN · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "VPN · 순서도 (업데이트)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "비밀번호 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.5, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 현재 접수된 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "프린터 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "메일함 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "접수 처리 중 · 상태와 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "접수 처리 중 · 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "티켓 T-1001은 처리 완료되었습니다. 새 배터리로 교체했습니다. 추가 도움이 필요하시면 연락 바랍니다.", + "sources": [], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "포털로 떠넘기지 않기", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/19-lab02-challenge2-before.txt b/Last3Mon/74/answers/19-lab02-challenge2-before.txt new file mode 100644 index 0000000..f602acd --- /dev/null +++ b/Last3Mon/74/answers/19-lab02-challenge2-before.txt @@ -0,0 +1,261 @@ +$ uv run tools/flow.py test cases/lab02-challenge2.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ 망가진 릴리스 2.1.0 · 400 release broken · 1.3s · status 200 (expected 400); error = null (expected release broken); prompt_version = null (expected p9) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ 평소 · 1.1.0은 저장소의 p2 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 운영(active 1.0.0)은 그대로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 4/5 +report: runs/lab02-challenge2-20261027-184109.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.1.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.2s +✔ VPN · 순서도 (업데이트) · 1.2s +✔ 비밀번호 · 해결 방법 · 1.5s +✔ 프린터 · 해결 방법 · 1.1s +✔ 메일함 · 해결 방법 · 1.3s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.0s +✔ 포털로 떠넘기지 않기 · 1.0s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184120.json +gate [release=2.1.0]: pass 9/9 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 2.1.0 promote diff --git a/Last3Mon/74/answers/20-lab02-challenge2.json b/Last3Mon/74/answers/20-lab02-challenge2.json new file mode 100644 index 0000000..01ed001 --- /dev/null +++ b/Last3Mon/74/answers/20-lab02-challenge2.json @@ -0,0 +1,206 @@ +{ + "cases": "74-lab02-challenge-golden.json", + "webhook": "my-helpdesk", + "passed": 0, + "total": 9, + "results": [ + { + "case": "VPN · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "route = null (expected 자동 답변)", + "answer has none of ['인증서']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "VPN · 순서도 (업데이트)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['업데이트', '최신']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "비밀번호 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['비밀번호 찾기', '휴대폰']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "프린터 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['대기열', '전원']" + ], + "seconds": 0.0, + "status": 400, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "메일함 · 해결 방법", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['보관함', '삭제']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "접수 처리 중 · 상태와 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['처리 중']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "접수 처리 중 · 예정일", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['10월 20일', '20일']" + ], + "seconds": 0.1, + "status": 400, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)", + "answer has none of ['처리 완료', '완료']" + ], + "seconds": 0.0, + "status": 400, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "포털로 떠넘기지 않기", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 400 (expected 200)" + ], + "seconds": 0.0, + "status": 400, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/20-lab02-challenge2.txt b/Last3Mon/74/answers/20-lab02-challenge2.txt new file mode 100644 index 0000000..6a84080 --- /dev/null +++ b/Last3Mon/74/answers/20-lab02-challenge2.txt @@ -0,0 +1,257 @@ +$ uv run tools/flow.py push answers/74-lab02-challenge2-broken.json +! unpublished '74 IT helpdesk + release API (reference)' (w2caac7c1a6981b8): it used the same webhook path +✔ pushed '74 IT helpdesk + broken release check (answer)' (id w86d00ad9781296e) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab02-challenge2.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 망가진 릴리스 2.1.0 · 400 release broken · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + } + ← response HTTP 400 + { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } +✔ 평소 · 1.1.0은 저장소의 p2 · 2.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 운영(active 1.0.0)은 그대로 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 5/5 +report: runs/lab02-challenge2-20261027-184139.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +$ uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.1.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✘ VPN · 해결 방법 · 0.1s · status 400 (expected 200); route = null (expected 자동 답변); answer has none of ['인증서'] +✘ VPN · 순서도 (업데이트) · 0.1s · status 400 (expected 200); answer has none of ['업데이트', '최신'] +✘ 비밀번호 · 해결 방법 · 0.1s · status 400 (expected 200); answer has none of ['비밀번호 찾기', '휴대폰'] +✘ 프린터 · 해결 방법 · 0.0s · status 400 (expected 200); answer has none of ['대기열', '전원'] +✘ 메일함 · 해결 방법 · 0.1s · status 400 (expected 200); answer has none of ['보관함', '삭제'] +✘ 접수 처리 중 · 상태와 예정일 · 0.1s · status 400 (expected 200); answer has none of ['처리 중'] +✘ 접수 처리 중 · 예정일 · 0.1s · status 400 (expected 200); answer has none of ['10월 20일', '20일'] +✘ 접수 처리 완료 · 0.0s · status 400 (expected 200); answer has none of ['처리 완료', '완료'] +✘ 포털로 떠넘기지 않기 · 0.0s · status 400 (expected 200) +Passed 0/9 +report: runs/74-lab02-challenge-golden-20261027-184140.json +gate [release=2.1.0]: pass 0/9 (0%) · p95 0.0s → FAIL · pass rate 0% < 100% +✔ gate record → /gates/helpdesk: 2.1.0 rollback diff --git a/Last3Mon/74/answers/21-lab02-after-challenge2.json b/Last3Mon/74/answers/21-lab02-after-challenge2.json new file mode 100644 index 0000000..c367562 --- /dev/null +++ b/Last3Mon/74/answers/21-lab02-after-challenge2.json @@ -0,0 +1,116 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소 · 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "저장소 고장 · 기본 지시문으로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "fallback" + } + } + }, + { + "case": "저장소 느림 · 오래 기다리지 않음", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "fallback" + } + } + }, + { + "case": "되돌린 뒤 · 다시 저장소에서", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/21-lab02-after-challenge2.txt b/Last3Mon/74/answers/21-lab02-after-challenge2.txt new file mode 100644 index 0000000..5e83fa9 --- /dev/null +++ b/Last3Mon/74/answers/21-lab02-after-challenge2.txt @@ -0,0 +1,24 @@ +$ uv run tools/flow.py test cases/lab02.json +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 1.3s +✔ 후보 시험 · 1.1.0 고정 · 1.1s +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.2s +✔ 없는 릴리스 · 0.1s +✔ 빈 질문 · 0.1s +Passed 5/5 +report: runs/lab02-20261027-184144.json + +$ uv run tools/flow.py test cases/lab01-challenge2.json +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.2s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 기본 지시문으로 답 · 1.2s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} +✔ 저장소 느림 · 오래 기다리지 않음 · 3.0s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 되돌린 뒤 · 다시 저장소에서 · 1.0s +Passed 4/4 +report: runs/lab01-challenge2-20261027-184150.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/74/answers/22-lab03-deploy.json b/Last3Mon/74/answers/22-lab03-deploy.json new file mode 100644 index 0000000..824dd7b --- /dev/null +++ b/Last3Mon/74/answers/22-lab03-deploy.json @@ -0,0 +1,62 @@ +{ + "cases": "lab03.json", + "webhook": "my-helpdesk", + "passed": 2, + "total": 2, + "results": [ + { + "case": "배포 · 1.1.0을 active로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "되돌리기 · 1.0.0", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/22-lab03-deploy.txt b/Last3Mon/74/answers/22-lab03-deploy.txt new file mode 100644 index 0000000..4d58e86 --- /dev/null +++ b/Last3Mon/74/answers/22-lab03-deploy.txt @@ -0,0 +1,260 @@ +$ uv run tools/flow.py release helpdesk 1.1.0 +✔ helpdesk: active 1.1.0 · no canary + 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + * 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +HTTP 200 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + +$ uv run tools/flow.py release helpdesk 1.0.0 +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +HTTP 200 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + +$ uv run tools/flow.py test cases/lab03.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 2 case(s) × 1 +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.1.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.1.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 배포 · 1.1.0을 active로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.0.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 되돌리기 · 1.0.0 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 2/2 +report: runs/lab03-20261027-184155.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } diff --git a/Last3Mon/74/answers/23-lab03-canary-before.json b/Last3Mon/74/answers/23-lab03-canary-before.json new file mode 100644 index 0000000..8171f5d --- /dev/null +++ b/Last3Mon/74/answers/23-lab03-canary-before.json @@ -0,0 +1,180 @@ +{ + "cases": "lab03-canary.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 6, + "results": [ + { + "case": "최유진 (bucket 0) → 카나리", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.release = 1.0.0 (expected 1.1.0)", + "version.channel = stable (expected canary)" + ], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "정우성 (bucket 42) → 카나리", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.release = 1.0.0 (expected 1.1.0)", + "version.channel = stable (expected canary)" + ], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "정우성" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "이준호 (bucket 56) → 운영", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "김민지 (bucket 80) → 운영", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "고정이 카나리보다 먼저", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "같은 사람은 늘 같은 쪽", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "version.release = 1.0.0 (expected 1.1.0)", + "version.channel = stable (expected canary)" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/23-lab03-canary-before.txt b/Last3Mon/74/answers/23-lab03-canary-before.txt new file mode 100644 index 0000000..ec6c339 --- /dev/null +++ b/Last3Mon/74/answers/23-lab03-canary-before.txt @@ -0,0 +1,259 @@ +$ uv run tools/flow.py release helpdesk --canary 1.1.0 --percent 50 +✔ helpdesk: active 1.0.0 · canary 1.1.0 50% + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + c 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + canary users: 최유진(0), 정우성(42) + +$ uv run tools/flow.py test cases/lab03-canary.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.1.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.1.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.1.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✘ 최유진 (bucket 0) → 카나리 · 1.1s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✘ 정우성 (bucket 42) → 카나리 · 1.3s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 (bucket 56) → 운영 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 (bucket 80) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 고정이 카나리보다 먼저 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✘ 같은 사람은 늘 같은 쪽 · 1.2s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 3/6 +report: runs/lab03-canary-20261027-184202.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } diff --git a/Last3Mon/74/answers/24-lab03-canary.json b/Last3Mon/74/answers/24-lab03-canary.json new file mode 100644 index 0000000..4167992 --- /dev/null +++ b/Last3Mon/74/answers/24-lab03-canary.json @@ -0,0 +1,135 @@ +{ + "cases": "lab02-challenge2.json", + "webhook": "my-helpdesk", + "passed": 5, + "total": 5, + "results": [ + { + "case": "망가진 릴리스 2.1.0 · 400 release broken", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 400, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + }, + "response": { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } + }, + { + "case": "평소 · 1.1.0은 저장소의 p2", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "저장소 고장 · 1.1.0도 기본 지시문으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } + }, + { + "case": "저장소 고장 · 없는 릴리스는 그대로 400", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + }, + "response": { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } + }, + { + "case": "운영(active 1.0.0)은 그대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/24-lab03-canary.txt b/Last3Mon/74/answers/24-lab03-canary.txt new file mode 100644 index 0000000..4b103e1 --- /dev/null +++ b/Last3Mon/74/answers/24-lab03-canary.txt @@ -0,0 +1,291 @@ +$ uv run tools/flow.py push reference/lab03-example2-canary.json +! unpublished '74 IT helpdesk + broken release check (answer)' (w86d00ad9781296e): it used the same webhook path +✔ pushed '74 IT helpdesk + canary (reference)' (id w2e1e5fc96604a25) + POST http://localhost:5678/webhook/my-helpdesk + +$ uv run tools/flow.py test cases/lab03-canary.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.1.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.1.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.1.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 최유진 (bucket 0) → 카나리 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 정우성 (bucket 42) → 카나리 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 문의해 주세요. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 이준호 (bucket 56) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 (bucket 80) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 고정이 카나리보다 먼저 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 같은 사람은 늘 같은 쪽 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +Passed 6/6 +report: runs/lab03-canary-20261027-184226.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +$ uv run tools/flow.py test cases/lab02.json +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 1.6s +✔ 후보 시험 · 1.1.0 고정 · 1.3s +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.4s +✔ 없는 릴리스 · 0.1s +✔ 빈 질문 · 0.1s +Passed 5/5 +report: runs/lab02-20261027-184231.json + +$ uv run tools/flow.py test cases/lab02-challenge2.json +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 망가진 릴리스 2.1.0 · 400 release broken · 0.1s +✔ 평소 · 1.1.0은 저장소의 p2 · 1.2s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.0s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 운영(active 1.0.0)은 그대로 · 1.1s +Passed 5/5 +report: runs/lab02-challenge2-20261027-184235.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + +$ uv run tools/flow.py release helpdesk --reset +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 diff --git a/Last3Mon/74/answers/25-lab03-watch.json b/Last3Mon/74/answers/25-lab03-watch.json new file mode 100644 index 0000000..eb88e75 --- /dev/null +++ b/Last3Mon/74/answers/25-lab03-watch.json @@ -0,0 +1,174 @@ +{ + "cases": "lab03-watch.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 6, + "results": [ + { + "case": "최유진 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['대기열']" + ], + "seconds": 0.9, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } + }, + { + "case": "정우성 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['대기열']" + ], + "seconds": 0.9, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + }, + "response": { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } + }, + { + "case": "한지훈 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "이준호 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "박서연 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "김민지 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/25-lab03-watch.txt b/Last3Mon/74/answers/25-lab03-watch.txt new file mode 100644 index 0000000..ac17b0a --- /dev/null +++ b/Last3Mon/74/answers/25-lab03-watch.txt @@ -0,0 +1,221 @@ +$ uv run tools/flow.py test cases/lab03-watch.json -v +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.2.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.2.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.2.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✘ 최유진 · 프린터 · 0.9s · answer has none of ['대기열'] + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✘ 정우성 · 프린터 · 0.9s · answer has none of ['대기열'] + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 한지훈 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 박서연 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 4/6 +report: runs/lab03-watch-20261027-184241.json + +$ uv run tools/flow.py release helpdesk +✔ helpdesk: active 1.0.0 · canary 1.2.0 50% + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + c 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + canary users: 최유진(0), 정우성(42) diff --git a/Last3Mon/74/answers/26-lab03-watch-after.json b/Last3Mon/74/answers/26-lab03-watch-after.json new file mode 100644 index 0000000..aae3ba4 --- /dev/null +++ b/Last3Mon/74/answers/26-lab03-watch-after.json @@ -0,0 +1,170 @@ +{ + "cases": "lab03-watch.json", + "webhook": "my-helpdesk", + "passed": 6, + "total": 6, + "results": [ + { + "case": "최유진 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "정우성 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "한지훈 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "이준호 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "박서연 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "김민지 · 프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/26-lab03-watch-after.txt b/Last3Mon/74/answers/26-lab03-watch-after.txt new file mode 100644 index 0000000..707c852 --- /dev/null +++ b/Last3Mon/74/answers/26-lab03-watch-after.txt @@ -0,0 +1,146 @@ +$ uv run tools/flow.py release helpdesk --no-canary +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +$ uv run tools/flow.py test cases/lab03-watch.json --no-before -v +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 최유진 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 정우성 · 프린터 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 한지훈 · 프린터 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 박서연 · 프린터 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 · 프린터 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 6/6 +report: runs/lab03-watch-20261027-184249.json + +$ uv run tools/flow.py release helpdesk --reset +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 diff --git a/Last3Mon/74/answers/27-lab03-challenge.json b/Last3Mon/74/answers/27-lab03-challenge.json new file mode 100644 index 0000000..838e202 --- /dev/null +++ b/Last3Mon/74/answers/27-lab03-challenge.json @@ -0,0 +1,68 @@ +{ + "cases": "lab03-challenge.json", + "webhook": "my-release-check", + "passed": 3, + "total": 3, + "results": [ + { + "case": "1.1.0 · 내보내도 됨", + "webhook": "my-release-check", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.1, + "status": 200, + "request": { + "release": "1.1.0" + }, + "response": { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 2, + "verdict": "promote" + } + }, + { + "case": "1.2.0 · 되돌림", + "webhook": "my-release-check", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.6, + "status": 200, + "request": { + "release": "1.2.0" + }, + "response": { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 1.6, + "verdict": "rollback" + } + }, + { + "case": "릴리스 없음", + "webhook": "my-release-check", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": {}, + "response": { + "error": "release is required" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/27-lab03-challenge.txt b/Last3Mon/74/answers/27-lab03-challenge.txt new file mode 100644 index 0000000..ad98897 --- /dev/null +++ b/Last3Mon/74/answers/27-lab03-challenge.txt @@ -0,0 +1,67 @@ +$ uv run tools/flow.py push answers/74-lab03-challenge-release-check.json +! unpublished '74 Release check + gate record (answer)' (w9589252e9498695): it used the same webhook path +✔ pushed '74 Release check (answer)' (id w50ae3687e1cc859) + POST http://localhost:5678/webhook/my-release-check + +$ uv run tools/flow.py test cases/lab03-challenge.json -v +POST http://localhost:5678/webhook/my-release-check · 3 case(s) × 1 +✔ 1.1.0 · 내보내도 됨 · 3.1s + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 2, + "verdict": "promote" + } +✔ 1.2.0 · 되돌림 · 1.6s + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 1.6, + "verdict": "rollback" + } +✔ 릴리스 없음 · 0.0s + → request POST /webhook/my-release-check + {} + ← response HTTP 400 + { + "error": "release is required" + } +Passed 3/3 +report: runs/lab03-challenge-20261027-184308.json + +$ uv run tools/flow.py call my-release-check '{"release": "2.0.0"}' -v +HTTP 200 · 1.9s + → request POST /webhook/my-release-check + { + "release": "2.0.0" + } + ← response HTTP 200 + { + "release": "2.0.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 1.8, + "verdict": "promote" + } diff --git a/Last3Mon/74/answers/28-lab03-challenge2-before.txt b/Last3Mon/74/answers/28-lab03-challenge2-before.txt new file mode 100644 index 0000000..c0c496c --- /dev/null +++ b/Last3Mon/74/answers/28-lab03-challenge2-before.txt @@ -0,0 +1,201 @@ +$ uv run tools/flow.py test cases/lab03-challenge2.json -v +✔ before: POST http://localhost:8800/gates/helpdesk/clear · HTTP 200 · 0.0s · {"cleared": 19} + → request POST http://localhost:8800/gates/helpdesk/clear + {} + ← response HTTP 200 + { + "cleared": 19 + } +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk/policy · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk/policy + { + "require_gate": true + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-release-check · 5 case(s) × 1 +✘ 1.2.0 점검 · 기록 · 24.2s · recorded = null (expected true) + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 24.1, + "verdict": "rollback" + } +✘ 1.1.0 점검 · 기록 · 7.9s · recorded = null (expected true) + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 7.9, + "verdict": "promote" + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.2.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.2.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.2.0", + "last_gate": null + } +✔ 점검에 떨어진 1.2.0은 배포 거부 (409) · 운영은 1.0.0 · 7.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✘ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.1.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.1.0", + "last_gate": null + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✘ the 'before' step failed (http://localhost:8800/releases/helpdesk). Is it pushed / is the data API the day 74 version? diff --git a/Last3Mon/74/answers/29-lab03-challenge2.json b/Last3Mon/74/answers/29-lab03-challenge2.json new file mode 100644 index 0000000..93641c8 --- /dev/null +++ b/Last3Mon/74/answers/29-lab03-challenge2.json @@ -0,0 +1,138 @@ +{ + "cases": "lab03-challenge2.json", + "webhook": "my-release-check", + "passed": 5, + "total": 5, + "results": [ + { + "case": "1.2.0 점검 · 기록", + "webhook": "my-release-check", + "run": 1, + "passed": true, + "problems": [], + "seconds": 10.6, + "status": 200, + "request": { + "release": "1.2.0" + }, + "response": { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 9.5, + "verdict": "rollback", + "recorded": true + } + }, + { + "case": "1.1.0 점검 · 기록", + "webhook": "my-release-check", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.9, + "status": 200, + "request": { + "release": "1.1.0" + }, + "response": { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 1.7, + "verdict": "promote", + "recorded": true + } + }, + { + "case": "점검에 떨어진 1.2.0은 배포 거부 (409) · 운영은 1.0.0", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 7.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "점검을 지난 1.1.0은 배포", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 문의해 주세요. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + }, + { + "case": "점검 안 한 2.0.0은 카나리도 거부 (409)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/29-lab03-challenge2.txt b/Last3Mon/74/answers/29-lab03-challenge2.txt new file mode 100644 index 0000000..63fa6b6 --- /dev/null +++ b/Last3Mon/74/answers/29-lab03-challenge2.txt @@ -0,0 +1,317 @@ +$ uv run tools/flow.py push answers/74-lab03-challenge2-gated.json +! unpublished '74 Release check (answer)' (w50ae3687e1cc859): it used the same webhook path +✔ pushed '74 Release check + gate record (answer)' (id w9589252e9498695) + POST http://localhost:5678/webhook/my-release-check + +$ uv run tools/flow.py test cases/lab03-challenge2.json -v +✔ before: POST http://localhost:8800/gates/helpdesk/clear · HTTP 200 · 0.0s · {"cleared": 0} + → request POST http://localhost:8800/gates/helpdesk/clear + {} + ← response HTTP 200 + { + "cleared": 0 + } +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk/policy · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk/policy + { + "require_gate": true + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-release-check · 5 case(s) × 1 +✔ 1.2.0 점검 · 기록 · 10.6s + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 9.5, + "verdict": "rollback", + "recorded": true + } +✔ 1.1.0 점검 · 기록 · 1.9s + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 1.7, + "verdict": "promote", + "recorded": true + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.2.0", "last_gate": {"t": 1793094252.829, "release": "1.2.0", "verdict": "rollback", + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.2.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.2.0", + "last_gate": { + "t": 1793094252.829, + "release": "1.2.0", + "verdict": "rollback", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 9.5, + "source": "my-release-check" + } + } +✔ 점검에 떨어진 1.2.0은 배포 거부 (409) · 운영은 1.0.0 · 7.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.1.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.1.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 점검을 지난 1.1.0은 배포 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 문의해 주세요. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "2.0.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "2.0.0", + "percent": 50 + } + } + ← response HTTP 409 + { + "error": "not gated", + "release": "2.0.0", + "last_gate": null + } +✔ 점검 안 한 2.0.0은 카나리도 거부 (409) · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 5/5 +report: runs/lab03-challenge2-20261027-184425.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +$ uv run tools/flow.py release helpdesk +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) diff --git a/Last3Mon/74/answers/30-lab03-gate-flow.json b/Last3Mon/74/answers/30-lab03-gate-flow.json new file mode 100644 index 0000000..f7cd1f6 --- /dev/null +++ b/Last3Mon/74/answers/30-lab03-gate-flow.json @@ -0,0 +1,172 @@ +{ + "cases": "golden.json", + "webhook": "my-helpdesk", + "passed": 6, + "total": 6, + "results": [ + { + "case": "VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "비밀번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. [FAQ-001] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "프린터", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. [FAQ-004] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "메일함", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보해 주시면 됩니다. [FAQ-005] \n접수 건은 찾지 못했습니다. 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 중", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1002의 현재 상태는 **처리 중**입니다. 메모에 따르면 **인증서 발급 승인 대기 중이며 10월 20일 완료 예정**입니다.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + }, + { + "case": "접수 처리 완료", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1001 어떻게 됐나요?", + "user": "이준호", + "release": "2.0.0" + }, + "response": { + "route": "자동 답변", + "answer": "T-1001은 **처리 완료** 상태이며, **새 배터리로 교체했습니다**. 추가로 확인이 필요하시면 담당자에게 문의해 주세요.", + "sources": [], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/74/answers/30-lab03-gate-flow.txt b/Last3Mon/74/answers/30-lab03-gate-flow.txt new file mode 100644 index 0000000..00de3d5 --- /dev/null +++ b/Last3Mon/74/answers/30-lab03-gate-flow.txt @@ -0,0 +1,53 @@ +$ uv run tools/flow.py release helpdesk --require-gate on +✔ helpdesk: active 1.0.0 · no canary · deploy needs a passing gate + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +$ uv run tools/flow.py release helpdesk 2.1.0 +✘ refused: 2.1.0 has no passing gate record (never gated) + → run the gate (flow.py gate ... --set release=) or my-release-check first + +$ uv run tools/flow.py gate cases/golden.json --set release=2.0.0 --max-p95 3 +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.1s +✔ 프린터 · 1.3s +✔ 메일함 · 1.4s +✔ 접수 처리 중 · 1.1s +✔ 접수 처리 완료 · 1.1s +Passed 6/6 +report: runs/golden-20261027-184432.json +gate [release=2.0.0]: pass 6/6 (100%) · p95 1.4s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote + +$ uv run tools/flow.py release helpdesk 2.0.0 +✔ helpdesk: active 2.0.0 · no canary · deploy needs a passing gate + 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + * 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 6/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +$ uv run tools/flow.py release helpdesk --reset +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 6/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +$ uv run tools/flow.py doctor +! n8n install folder /Users/mcchae/day65-n8n + → day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더> +✔ n8n container day65-n8n-n8n-1 +✔ n8n http://localhost:5678 +✔ data API http://localhost:8800 +✔ releases /releases/helpdesk · gates /gates/helpdesk (day 74 data API) +✔ n8n → http://data-api:8800 +✔ credential 'OpenAI (.env)' (openAiCourse0001) +✔ node catalog (for check) +✔ CLI coding agent: codex, claude, agy +ready diff --git a/Last3Mon/74/answers/74-lab01-challenge-registry.json b/Last3Mon/74/answers/74-lab01-challenge-registry.json new file mode 100644 index 0000000..5ff4599 --- /dev/null +++ b/Last3Mon/74/answers/74-lab01-challenge-registry.json @@ -0,0 +1,357 @@ +{ + "name": "74 IT helpdesk + prompt registry (answer)", + "nodes": [ + { + "name": "Question webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-helpdesk", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2", + "position": [ + 0, + 300 + ], + "id": "d898f230-a253-55da-9ec5-4288291c08dd" + }, + { + "name": "Has question?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.question }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "185f04ea-0ee8-514b-b3fd-11b89b1c0c2e" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"question is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "8a3b377f-f3fa-56e8-a9c4-98a3e014ce61" + }, + { + "name": "Release info", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "parameters": { + "assignments": { + "assignments": [ + { + "id": "r1", + "name": "release", + "value": "1.0.0", + "type": "string" + }, + { + "id": "r2", + "name": "prompt_version", + "value": "p1", + "type": "string" + }, + { + "id": "r3", + "name": "model", + "value": "gpt-4.1-mini", + "type": "string" + } + ] + }, + "options": {} + }, + "position": [ + 440, + 300 + ], + "id": "b33700b3-849d-5b4f-8d8c-42723207125b" + }, + { + "name": "Get prompt", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/prompts/helpdesk/{{ $('Release info').first().json.prompt_version }}", + "options": {} + }, + "position": [ + 660, + 300 + ], + "id": "a39d24f1-c645-50c9-940e-59e381744fc1" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": {}, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "position": [ + 880, + 300 + ], + "id": "c7fa8ee8-ba2b-5266-9d1a-ffa5dd8743b5" + }, + { + "name": "Get ticket", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "position": [ + 1100, + 300 + ], + "id": "b4fd19f5-03d4-5b31-a654-4b0ac4c32fac" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}", + "options": { + "systemMessage": "={{ $('Get prompt').first().json.text }}", + "maxIterations": 1 + } + }, + "position": [ + 1320, + 300 + ], + "id": "1a80728e-83e0-58d7-9477-42d4099502c6" + }, + { + "name": "Answer model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "={{ $('Release info').first().json.model }}" + }, + "options": { + "temperature": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 1320, + 520 + ], + "id": "4c69869f-3300-5834-8124-e64dfcc8135e" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nconst i = $('Release info').first().json;\nresult.version = { release: i.release, prompt_version: i.prompt_version, model: i.model, prompt_source: 'registry' };\nreturn [{ json: result }];" + }, + "position": [ + 1540, + 300 + ], + "id": "7c284309-570a-5987-b139-e1be350994fd" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1760, + 300 + ], + "id": "32a9aaf4-2a59-5388-8a24-0e9f28c346e4" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Release info", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Release info": { + "main": [ + [ + { + "node": "Get prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get prompt": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/answers/74-lab01-challenge2-fallback.json b/Last3Mon/74/answers/74-lab01-challenge2-fallback.json new file mode 100644 index 0000000..a574d5d --- /dev/null +++ b/Last3Mon/74/answers/74-lab01-challenge2-fallback.json @@ -0,0 +1,435 @@ +{ + "name": "74 IT helpdesk + prompt fallback (answer)", + "nodes": [ + { + "name": "Question webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-helpdesk", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2", + "position": [ + 0, + 300 + ], + "id": "956f6ead-f979-5d78-bedf-13743406c6b5" + }, + { + "name": "Has question?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.question }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "b946f570-248c-5436-a6d6-06ab2b15db29" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"question is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "e9818b28-ea3d-517c-ab00-02f86427c3c5" + }, + { + "name": "Release info", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "parameters": { + "assignments": { + "assignments": [ + { + "id": "r1", + "name": "release", + "value": "1.0.0", + "type": "string" + }, + { + "id": "r2", + "name": "prompt_version", + "value": "p1", + "type": "string" + }, + { + "id": "r3", + "name": "model", + "value": "gpt-4.1-mini", + "type": "string" + } + ] + }, + "options": {} + }, + "position": [ + 440, + 300 + ], + "id": "0ffcc09f-8eff-56b4-801e-9c8e2a40930c" + }, + { + "name": "Get prompt", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/prompts/helpdesk/{{ $('Release info').first().json.prompt_version }}", + "options": { + "timeout": 2000 + } + }, + "position": [ + 660, + 300 + ], + "id": "578695ac-1a56-57c4-863b-08677838f02c", + "onError": "continueErrorOutput" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": {}, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "position": [ + 1100, + 300 + ], + "id": "322d506e-a6a2-52bc-b153-170da84009c2" + }, + { + "name": "Get ticket", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "position": [ + 1320, + 300 + ], + "id": "3caf9b96-2920-54de-a905-3eabacb6f6e9" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}", + "options": { + "systemMessage": "={{ $('Registry prompt').isExecuted ? $('Registry prompt').first().json.text : $('Fallback prompt').first().json.text }}", + "maxIterations": 1 + } + }, + "position": [ + 1540, + 300 + ], + "id": "500a6055-13cd-5000-9255-4f854efc5bb5" + }, + { + "name": "Answer model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "={{ $('Release info').first().json.model }}" + }, + "options": { + "temperature": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 1540, + 520 + ], + "id": "efcecd80-eb64-54b9-ba80-23ce47e0d416" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nconst i = $('Release info').first().json;\nresult.version = { release: i.release, prompt_version: i.prompt_version, model: i.model, prompt_source: ($('Registry prompt').isExecuted ? 'registry' : 'fallback') };\nreturn [{ json: result }];" + }, + "position": [ + 1760, + 300 + ], + "id": "999ca8b6-3571-5e6d-8c0d-d681a8cf40ee" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1980, + 300 + ], + "id": "f9bbb5e8-0e9c-5c3b-9a23-204b66884821" + }, + { + "name": "Registry prompt", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 880, + 300 + ], + "id": "8161625d-2c7f-5acc-9377-3545a0ced05c", + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f0", + "name": "text", + "value": "={{ $json.text }}", + "type": "string" + } + ] + }, + "options": {} + } + }, + { + "name": "Fallback prompt", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 880, + 540 + ], + "id": "d9000207-de8d-561e-865c-216430daeaa5", + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f0", + "name": "text", + "value": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.", + "type": "string" + } + ] + }, + "options": {} + } + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Release info", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Release info": { + "main": [ + [ + { + "node": "Get prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get prompt": { + "main": [ + [ + { + "node": "Registry prompt", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fallback prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Registry prompt": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fallback prompt": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/answers/74-lab02-challenge-golden.json b/Last3Mon/74/answers/74-lab02-challenge-golden.json new file mode 100644 index 0000000..abbaeae --- /dev/null +++ b/Last3Mon/74/answers/74-lab02-challenge-golden.json @@ -0,0 +1,114 @@ +{ + "webhook": "my-helpdesk", + "cases": [ + { + "name": "VPN · 해결 방법", + "request": "requests/faq-vpn.json", + "expect": { + "equals": { + "route": "자동 답변" + }, + "contains": { + "answer": [ + "인증서" + ] + } + } + }, + { + "name": "VPN · 순서도 (업데이트)", + "request": "requests/faq-vpn.json", + "expect": { + "contains": { + "answer": [ + "업데이트", + "최신" + ] + } + } + }, + { + "name": "비밀번호 · 해결 방법", + "request": "requests/faq-password.json", + "expect": { + "contains": { + "answer": [ + "비밀번호 찾기", + "휴대폰" + ] + } + } + }, + { + "name": "프린터 · 해결 방법", + "request": "requests/faq-printer.json", + "expect": { + "contains": { + "answer": [ + "대기열", + "전원" + ] + } + } + }, + { + "name": "메일함 · 해결 방법", + "request": "requests/faq-mail.json", + "expect": { + "contains": { + "answer": [ + "보관함", + "삭제" + ] + } + } + }, + { + "name": "접수 처리 중 · 상태와 예정일", + "request": "requests/ticket-own.json", + "expect": { + "contains": { + "answer": [ + "처리 중" + ] + } + } + }, + { + "name": "접수 처리 중 · 예정일", + "request": "requests/ticket-own.json", + "expect": { + "contains": { + "answer": [ + "10월 20일", + "20일" + ] + } + } + }, + { + "name": "접수 처리 완료", + "request": "requests/ticket-done.json", + "expect": { + "contains": { + "answer": [ + "처리 완료", + "완료" + ] + } + } + }, + { + "name": "포털로 떠넘기지 않기", + "request": "requests/faq-printer.json", + "expect": { + "not_contains": { + "answer": [ + "IT 포털을 확인", + "포털을 참고" + ] + } + } + } + ] +} diff --git a/Last3Mon/74/answers/74-lab02-challenge2-broken.json b/Last3Mon/74/answers/74-lab02-challenge2-broken.json new file mode 100644 index 0000000..e5fd076 --- /dev/null +++ b/Last3Mon/74/answers/74-lab02-challenge2-broken.json @@ -0,0 +1,571 @@ +{ + "name": "74 IT helpdesk + broken release check (answer)", + "nodes": [ + { + "name": "Question webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-helpdesk", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "de518e93-ed19-541f-a6fc-687a4efaf1b2", + "position": [ + 0, + 300 + ], + "id": "a45a7cee-22e6-5b71-a0fd-3229330b4bbc" + }, + { + "name": "Has question?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.question }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "299d8e82-eeab-5f99-9a46-137788c8ebb9" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"question is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "03fcdd93-2dbb-572f-b5fb-1c9ff133f3ea" + }, + { + "name": "Get release", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/releases/helpdesk", + "options": {} + }, + "position": [ + 440, + 300 + ], + "id": "03108783-0952-544a-a765-fa3f085af473" + }, + { + "name": "Pick release", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const body = $('Question webhook').first().json.body;\nconst r = $json;\nconst bucket = [...String(body.user ?? '')].reduce((sum, ch) => sum + ch.codePointAt(0), 0) % 100;\nlet release = r.active, channel = 'stable';\nif (body.release) { release = String(body.release); channel = 'pinned'; }\n\nconst info = r.releases?.[release];\nif (!info) return [{ json: { ok: false, release, known: Object.keys(r.releases ?? {}) } }];\nreturn [{ json: { ok: true, release, channel, bucket, prompt_version: info.prompt_version, model: info.model } }];" + }, + "position": [ + 660, + 300 + ], + "id": "c8b634b5-7537-561f-95ea-c96ef4bca3f5" + }, + { + "name": "Known release?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.ok }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 880, + 300 + ], + "id": "1ed7677b-7a5f-51d2-a53e-ba8492a36fd4" + }, + { + "name": "Reject release", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify({ error: 'unknown release', release: $json.release, known: $json.known }) }}", + "options": { + "responseCode": 400 + } + }, + "position": [ + 1100, + 560 + ], + "id": "64c8dfc5-03fb-5c28-b8b9-1982526a211a" + }, + { + "name": "Get prompt", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/prompts/helpdesk/{{ $('Pick release').first().json.prompt_version }}", + "options": { + "timeout": 2000, + "response": { + "response": { + "neverError": true, + "fullResponse": true + } + } + } + }, + "position": [ + 1100, + 300 + ], + "id": "77e6bc79-43c9-530b-8b64-905fa6e2d7b8", + "onError": "continueErrorOutput" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": {}, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "position": [ + 1760, + 300 + ], + "id": "255ba11b-b6c8-5d20-81d3-0ef7031b9f53" + }, + { + "name": "Get ticket", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "position": [ + 1980, + 300 + ], + "id": "b1e5fa0b-afae-5885-a4cf-e0703d11057d" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}", + "options": { + "systemMessage": "={{ $('Registry prompt').isExecuted ? $('Registry prompt').first().json.text : $('Fallback prompt').first().json.text }}", + "maxIterations": 1 + } + }, + "position": [ + 2200, + 300 + ], + "id": "3ef8a476-ca75-5139-8560-67ef9c9f2e50" + }, + { + "name": "Answer model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "={{ $('Pick release').first().json.model }}" + }, + "options": { + "temperature": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 2200, + 520 + ], + "id": "1dfcbaad-7d59-5856-be0c-5d2321a43364" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nconst i = $('Pick release').first().json;\nresult.version = { release: i.release, prompt_version: i.prompt_version, model: i.model, channel: i.channel, prompt_source: ($('Registry prompt').isExecuted && $('Registry prompt').first().json.state === 'registry') ? 'registry' : 'fallback' };\nreturn [{ json: result }];" + }, + "position": [ + 2420, + 300 + ], + "id": "96b32475-5056-5d64-9d49-a4422d523aa4" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 2640, + 300 + ], + "id": "45eb0e50-740f-5969-9ddf-af23290e4484" + }, + { + "name": "Registry prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1320, + 300 + ], + "id": "516ccc9d-19d1-5b72-b514-55df1f1c6426", + "parameters": { + "jsCode": "// 200: the registry prompt. 404: the release names a prompt that does not exist (a broken release).\n// anything else (500 ...): an outage, so use the built-in prompt like the error output does.\nconst status = $json.statusCode;\nif (status === 200 && $json.body?.text) return [{ json: { state: 'registry', text: $json.body.text } }];\nif (status === 404) return [{ json: { state: 'broken' } }];\nreturn [{ json: { state: 'fallback', text: \"You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \\\"찾지 못했습니다\\\" and that a staff member will help. Use 합니다체.\" } }];" + } + }, + { + "name": "Fallback prompt", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1320, + 540 + ], + "id": "21a69db0-9a21-5f97-94a5-55679b268089", + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f0", + "name": "text", + "value": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.", + "type": "string" + } + ] + }, + "options": {} + } + }, + { + "name": "Prompt broken?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1540, + 300 + ], + "id": "9d349380-cde8-5736-ae9c-301eac84eb07", + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.state }}", + "rightValue": "broken", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "name": "Reject broken", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 1760, + 780 + ], + "id": "d1579bb1-e209-584f-a9f4-89d779cdad40", + "parameters": { + "respondWith": "json", + "options": { + "responseCode": 400 + }, + "responseBody": "={{ JSON.stringify({ error: 'release broken', release: $('Pick release').first().json.release, prompt_version: $('Pick release').first().json.prompt_version }) }}" + } + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Get release", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get release": { + "main": [ + [ + { + "node": "Pick release", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pick release": { + "main": [ + [ + { + "node": "Known release?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Known release?": { + "main": [ + [ + { + "node": "Get prompt", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject release", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get prompt": { + "main": [ + [ + { + "node": "Registry prompt", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fallback prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Registry prompt": { + "main": [ + [ + { + "node": "Prompt broken?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fallback prompt": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prompt broken?": { + "main": [ + [ + { + "node": "Reject broken", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/answers/74-lab03-challenge-release-check.json b/Last3Mon/74/answers/74-lab03-challenge-release-check.json new file mode 100644 index 0000000..feb8642 --- /dev/null +++ b/Last3Mon/74/answers/74-lab03-challenge-release-check.json @@ -0,0 +1,206 @@ +{ + "name": "74 Release check (answer)", + "nodes": [ + { + "name": "Check webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-release-check", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "32c84fbc-d108-5979-a1bf-a11fb0028d96", + "position": [ + 0, + 300 + ], + "id": "cf6ae2a5-a340-50d9-8826-86dcde970c8a" + }, + { + "name": "Has release?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.release }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "07285064-a3b8-5605-a6ee-580e64e38fbc" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"release is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "34e77f24-bb35-5cf2-80e3-457681e2bd0e" + }, + { + "name": "Golden questions", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const release = String($('Check webhook').first().json.body.release);\nconst golden = [\n { question: 'VPN 접속이 자꾸 끊겨요. 어떻게 하나요?', user: '이준호', expect: ['인증서'] },\n { question: '비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?', user: '한지훈', expect: ['비밀번호 찾기', '휴대폰'] },\n { question: '프린터 출력이 안 돼요', user: '최유진', expect: ['대기열', '전원'] },\n { question: '메일함 용량이 꽉 찼대요', user: '김민지', expect: ['보관함', '삭제'] },\n { question: 'T-1002 처리 상태 알려 주세요', user: '박서연', expect: ['처리 중'] },\n { question: 'T-1001 어떻게 됐나요?', user: '이준호', expect: ['처리 완료', '완료'] },\n];\nreturn golden.map((g) => ({ json: { ...g, release, t0: Date.now() } }));" + }, + "position": [ + 440, + 300 + ], + "id": "9f9a1078-aab9-55d5-ac81-4292f7d22e73" + }, + { + "name": "Ask helpdesk", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://localhost:5678/webhook/my-helpdesk", + "options": { + "response": { + "response": { + "neverError": true + } + }, + "timeout": 30000 + }, + "method": "POST", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ question: $json.question, user: $json.user, release: $json.release }) }}" + }, + "position": [ + 660, + 300 + ], + "id": "30940a89-e2b9-5153-bb09-9eca37792e8a" + }, + { + "name": "Judge", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const golden = $('Golden questions').all().map((item) => item.json);\nconst answers = $input.all().map((item) => item.json);\nconst rows = golden.map((g, i) => {\n const a = answers[i] ?? {};\n const text = String(a.answer ?? '');\n const ok = !a.error && g.expect.some((word) => text.includes(word)) && (a.version?.release === g.release);\n return { question: g.question, ok, release_seen: a.version?.release ?? null };\n});\nconst passed = rows.filter((row) => row.ok).length;\nconst failed = rows.filter((row) => !row.ok).map((row) => row.question);\nconst seconds = Math.round((Date.now() - golden[0].t0) / 100) / 10;\nreturn [{ json: { release: golden[0].release, passed, total: rows.length, failed, seconds,\n verdict: passed === rows.length ? 'promote' : 'rollback' } }];" + }, + "position": [ + 880, + 300 + ], + "id": "5cbfc582-550a-5e91-aab4-bf3df3e17fcf" + }, + { + "name": "Send verdict", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1100, + 300 + ], + "id": "f93d3e49-5d3d-5a46-afb6-c694f03e65ee" + } + ], + "connections": { + "Check webhook": { + "main": [ + [ + { + "node": "Has release?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has release?": { + "main": [ + [ + { + "node": "Golden questions", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Golden questions": { + "main": [ + [ + { + "node": "Ask helpdesk", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ask helpdesk": { + "main": [ + [ + { + "node": "Judge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Judge": { + "main": [ + [ + { + "node": "Send verdict", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/answers/74-lab03-challenge2-gated.json b/Last3Mon/74/answers/74-lab03-challenge2-gated.json new file mode 100644 index 0000000..86e47f4 --- /dev/null +++ b/Last3Mon/74/answers/74-lab03-challenge2-gated.json @@ -0,0 +1,243 @@ +{ + "name": "74 Release check + gate record (answer)", + "nodes": [ + { + "name": "Check webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-release-check", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "32c84fbc-d108-5979-a1bf-a11fb0028d96", + "position": [ + 0, + 300 + ], + "id": "d961a9dc-450e-5d96-8451-dbc5fca3ba66" + }, + { + "name": "Has release?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.release }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "2ba94bc7-bef4-51e1-b534-669e49072171" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"release is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "87efe7b5-90b5-5674-8e8b-5e28a9cca01c" + }, + { + "name": "Golden questions", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const release = String($('Check webhook').first().json.body.release);\nconst golden = [\n { question: 'VPN 접속이 자꾸 끊겨요. 어떻게 하나요?', user: '이준호', expect: ['인증서'] },\n { question: '비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?', user: '한지훈', expect: ['비밀번호 찾기', '휴대폰'] },\n { question: '프린터 출력이 안 돼요', user: '최유진', expect: ['대기열', '전원'] },\n { question: '메일함 용량이 꽉 찼대요', user: '김민지', expect: ['보관함', '삭제'] },\n { question: 'T-1002 처리 상태 알려 주세요', user: '박서연', expect: ['처리 중'] },\n { question: 'T-1001 어떻게 됐나요?', user: '이준호', expect: ['처리 완료', '완료'] },\n];\nreturn golden.map((g) => ({ json: { ...g, release, t0: Date.now() } }));" + }, + "position": [ + 440, + 300 + ], + "id": "9b1783b9-375e-509d-8990-992a70b79062" + }, + { + "name": "Ask helpdesk", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://localhost:5678/webhook/my-helpdesk", + "options": { + "response": { + "response": { + "neverError": true + } + }, + "timeout": 30000 + }, + "method": "POST", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ question: $json.question, user: $json.user, release: $json.release }) }}" + }, + "position": [ + 660, + 300 + ], + "id": "b2944162-4641-5f43-9468-89d4d3cab67d" + }, + { + "name": "Judge", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const golden = $('Golden questions').all().map((item) => item.json);\nconst answers = $input.all().map((item) => item.json);\nconst rows = golden.map((g, i) => {\n const a = answers[i] ?? {};\n const text = String(a.answer ?? '');\n const ok = !a.error && g.expect.some((word) => text.includes(word)) && (a.version?.release === g.release);\n return { question: g.question, ok, release_seen: a.version?.release ?? null };\n});\nconst passed = rows.filter((row) => row.ok).length;\nconst failed = rows.filter((row) => !row.ok).map((row) => row.question);\nconst seconds = Math.round((Date.now() - golden[0].t0) / 100) / 10;\nreturn [{ json: { release: golden[0].release, passed, total: rows.length, failed, seconds,\n verdict: passed === rows.length ? 'promote' : 'rollback' } }];" + }, + "position": [ + 880, + 300 + ], + "id": "00c58707-3b5f-5880-921e-b664bb6c6001" + }, + { + "name": "Send verdict", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify({ ...$('Judge').first().json, recorded: $json.stored === true }) }}", + "options": {} + }, + "position": [ + 1320, + 300 + ], + "id": "548d6719-91e4-5920-ba38-2abb01730d92" + }, + { + "name": "Record gate", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1100, + 300 + ], + "id": "37a5dc22-d6e9-58c7-ac41-fdafcd25d9d0", + "onError": "continueRegularOutput", + "parameters": { + "method": "POST", + "url": "http://data-api:8800/gates/helpdesk", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ release: $json.release, verdict: $json.verdict, passed: $json.passed, total: $json.total, failed: $json.failed, seconds: $json.seconds, source: 'my-release-check' }) }}", + "options": { + "timeout": 3000, + "response": { + "response": { + "neverError": true + } + } + } + } + } + ], + "connections": { + "Check webhook": { + "main": [ + [ + { + "node": "Has release?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has release?": { + "main": [ + [ + { + "node": "Golden questions", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Golden questions": { + "main": [ + [ + { + "node": "Ask helpdesk", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ask helpdesk": { + "main": [ + [ + { + "node": "Judge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Judge": { + "main": [ + [ + { + "node": "Record gate", + "type": "main", + "index": 0 + } + ] + ] + }, + "Record gate": { + "main": [ + [ + { + "node": "Send verdict", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/answers/LAB-TEST-RESULT.md b/Last3Mon/74/answers/LAB-TEST-RESULT.md new file mode 100644 index 0000000..7cac030 --- /dev/null +++ b/Last3Mon/74/answers/LAB-TEST-RESULT.md @@ -0,0 +1,4290 @@ +# 74일차 Lab 테스트 결과 · 실제로 만든 시나리오와 `test -v` 결과 + +> **정답 자료** · 74일차 n8n 워크플로우를 **만드는 프롬프트(codex) → push → `call -v` · `bench` · `test -v` · `gate` · `release` 결과**를 학생이 가는 순서대로 한 문서에 모았습니다. 수업이 끝난 뒤 공개합니다. + +- 실행: 2026-10-27, 실습을 Lab마다 5개(예제 3 + 과제 2)로 늘린 뒤 강사 노트북의 65일차 n8n 2.42.6 + 74일차 판 데이터 API(지시문 저장소 · 릴리스 · 점검 기록 · 배포 정책), OpenAI `gpt-4.1-mini` · `gpt-5.4-nano`, 케이스 파일마다 **한 번씩** (Lab 01 예제 3 · Lab 02 예제 3만 `--repeat 3`), 모두 31단계 (00~30) +- 명령: `~/aio-01d4-stu/Last3Mon/74`에서 `uv run tools/flow.py ...`. 아래 단계마다 **실행한 명령**과 **화면에 나온 출력 전부**를 그대로 붙이고, 그 아래 **쉽게 말하면**으로 결과를 풀어 썼습니다 (특히 ✘가 나온 까닭과 그것이 예상한 것인지). 같은 출력이 `NN-<이름>.txt`에, `test`의 마지막 보고서가 `NN-<이름>.json`에 있습니다 (`diff` · `call` · `bench`만 한 단계와 28단계는 보고서 없음). +- LLM 문장과 **시간은 실행마다 다릅니다.** 이번 실행에서 정답의 잘못으로 실패한 케이스는 없습니다. ✘는 모두 "고치기 전" 단계와 일부러 보여 주는 단계(나쁜 릴리스 · 나쁜 카나리)에서 본 것입니다. +- 수업 준비 중 실제로 일어난 일: v2와 2.0.0의 작은 모델 `gpt-4.1-nano`가 OpenAI에서 **지원 종료**되어 500이 났습니다. `gpt-5.4-nano`로 바꾼 뒤 처음부터 다시 돌린 기록입니다. + +## `-v`가 보여 주는 것 + +```text +✔ 후보 시험 · 1.1.0 고정 · 1.6s ← 케이스 이름 · 시간 (틀리면 틀린 곳) + → request POST /webhook/my-helpdesk ← 보낸 요청 JSON (--set release=... 이 더해진 것) + { "question": "VPN …", "user": "이준호", "release": "1.1.0" } + ← response HTTP 200 ← 받은 응답 JSON + { "route": "자동 답변", "answer": "… [FAQ-003]", "version": { "release": "1.1.0", "channel": "pinned", … } } +``` + +오늘은 응답의 **`version`**(release · prompt_version · model · channel · prompt_source)을 봐야 **어느 릴리스가 답했는지** 알 수 있습니다. + +## 이 문서 하나로 따라가기 + +단계마다 **① codex에 넣는 프롬프트 → ② codex가 `my-work/*.json`(또는 `cases/my-golden.json`)을 쓰고 `check` → ③ 내가 `push` → ④ 내가 `test -v` · `gate` · `release`** 순서입니다. 재기만 하는 예제(Lab 01 예제 1 · 3, Lab 02 예제 2 · 3, Lab 03 예제 1 · 3)와 고치기 전 단계는 프롬프트가 없습니다. + +```mermaid +flowchart LR + P["① 프롬프트
터미널 ① codex"] --> C["② codex
my-work/*.json · check"] --> U["③ push
터미널 ②"] --> T["④ test -v · gate · release
version · 시간"] + T -->|✘| X["실패 줄 · 보고서 경로를
codex에 그대로"] --> C +``` + +- **프롬프트는 정답본(`LAB-0x-A-*.md`)의 요청문 예**를 그대로 썼습니다 (실습 문서 빈칸 채우기 뼈대의 `____`를 채운 글자라 빈칸 자리에 띄어쓰기가 남아 있습니다). +- **이 기록의 워크플로우는 수업 준비 때 만든 정답 파일**입니다. 프롬프트를 codex에 보내 다시 만들어 재지는 않았습니다 (68일차부터 수업 준비는 정답 워크플로우만 실행). +- 학생은 `push my-work/helpdesk.json`, 이 기록은 같은 웹훅(`my-helpdesk`)의 정답 파일을 push했습니다. 그래서 `test` 명령과 결과 모양은 같습니다. + +## 시나리오 한눈에 + +```mermaid +flowchart LR + L1["Lab 01 (00~12)
diff 7개
v1 9/15 · v2 15/15 (×3)
과제 2 2/4 → 4/4"] --> L2["Lab 02 (13~21)
약한 golden 1.2.0 PASS
강한 golden 1.2.0 1/9 FAIL
2.1.0 PASS → FAIL"] + L2 --> L3["Lab 03 (22~30)
카나리 3/6 → 6/6
나쁜 카나리 4/6 → 6/6
점검 기록 · 정책 5/5"] +``` + +| 단계 | 먼저 올린 것 (`push`) | 테스트 · 재기 | 결과 | 파일 | +| --- | --- | --- | --- | --- | +| 00 | `reference/helpdesk-v1.json` · `v2.json` | `release --reset` · `doctor` | ready | `00-prep` | +| 01 | (없음) | `diff` · `diff --full` | 7개 | `01-lab01-diff` | +| 02 | (같음) | `call rel-v1` · `rel-v2` | 2.8초 / 1.0초 `[FAQ-003]` | `02-lab01-call-v1-v2` | +| 03 | `reference/lab01-example2-version.json` | `lab01` | 3/3 | `03-lab01-version` | +| 04 | (같음) | `lab01-compare --webhook rel-v1 --repeat 3` | 9/15 | `04-lab01-compare-v1` | +| 05 | (같음) | `lab01-compare --webhook rel-v2 --repeat 3` | 15/15 | `05-lab01-compare-v2` | +| 06 | (같음) | `bench rel-v1` · `rel-v2` | p50 1.23 / 0.98초 | `06-lab01-bench-v1-v2` | +| 07 | (같음) | `lab01-challenge` | 2/3 | `07-lab01-challenge-before` | +| 08 | `answers/74-lab01-challenge-registry.json` | `lab01-challenge` | 3/3 | `08-lab01-challenge` | +| 09 | (같음) | `lab01` | 3/3 | `09-lab01-after-challenge` | +| 10 | (같음) | `lab01-challenge2` | 2/4 | `10-lab01-challenge2-before` | +| 11 | `answers/74-lab01-challenge2-fallback.json` | `lab01-challenge2` | 4/4 | `11-lab01-challenge2` | +| 12 | (같음) | `lab01-challenge` | 3/3 | `12-lab01-challenge-after2` | +| 13 | (같음) | `release` · `lab02` | 1/5 | `13-lab02-before` | +| 14 | `reference/lab02-example1-release.json` | `lab02` | 5/5 | `14-lab02-release` | +| 15 | (같음) | `lab01-challenge` · `lab01-challenge2` | 3/3 · 4/4 | `15-lab02-lab01-again` | +| 16 | (같음) | `gate golden` × 4 · `call 1.2.0` | 모두 PASS | `16-lab02-golden` | +| 17 | (같음) | `bench --set` × 2 · `gate --repeat 3` | 1.23 / 1.12초 · 18/18 | `17-lab02-baseline` | +| 18 | (같음) | `gate my-golden` × 4 | 1.2.0만 FAIL 1/9 | `18-lab02-my-golden` | +| 19 | (같음) | `lab02-challenge2` · `gate my-golden 2.1.0` | 4/5 · PASS | `19-lab02-challenge2-before` | +| 20 | `answers/74-lab02-challenge2-broken.json` | `lab02-challenge2` · `gate my-golden 2.1.0` | 5/5 · FAIL | `20-lab02-challenge2` | +| 21 | (같음) | `lab02` · `lab01-challenge2` | 5/5 · 4/4 | `21-lab02-after-challenge2` | +| 22 | (같음) | `release` · `call` × 2 · `lab03` | 2/2 | `22-lab03-deploy` | +| 23 | (같음) | `release --canary` · `lab03-canary` | 3/6 | `23-lab03-canary-before` | +| 24 | `reference/lab03-example2-canary.json` | `lab03-canary` · `lab02` · `lab02-challenge2` | 6/6 · 5/5 · 5/5 | `24-lab03-canary` | +| 25 | (같음) | `lab03-watch` | 4/6 | `25-lab03-watch` | +| 26 | (같음) | `--no-canary` · `lab03-watch --no-before` | 6/6 | `26-lab03-watch-after` | +| 27 | `answers/74-lab03-challenge-release-check.json` | `lab03-challenge` · `call 2.0.0` | 3/3 · promote | `27-lab03-challenge` | +| 28 | (같음) | `lab03-challenge2` | 점검 둘 ✘ · 1.1.0 배포 409로 멈춤 | `28-lab03-challenge2-before` | +| 29 | `answers/74-lab03-challenge2-gated.json` | `lab03-challenge2` | 5/5 | `29-lab03-challenge2` | +| 30 | (같음) | `--require-gate on` · `gate` · `release` · `doctor` | 2.1.0 거부 · 2.0.0 배포 · ready | `30-lab03-gate-flow` | + +## 준비 + +### 00 · 수업 준비 · 처음 상태 · v1 · v2 게시 · doctor + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk --reset +uv run tools/flow.py push reference/helpdesk-v1.json reference/helpdesk-v2.json +uv run tools/flow.py doctor +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate promote 6/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate rollback 0/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +✔ pushed '74 IT helpdesk v1 (provided)' (id w0ed73a832099852) + POST http://localhost:5678/webhook/rel-v1 +✔ pushed '74 IT helpdesk v2 (provided)' (id w8e65b72e125f790) + POST http://localhost:5678/webhook/rel-v2 + +! n8n install folder /Users/mcchae/day65-n8n + → day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더> +✔ n8n container day65-n8n-n8n-1 +✔ n8n http://localhost:5678 +✔ data API http://localhost:8800 +✔ releases /releases/helpdesk · gates /gates/helpdesk (day 74 data API) +✔ n8n → http://data-api:8800 +✔ credential 'OpenAI (.env)' (openAiCourse0001) +✔ node catalog (for check) +✔ CLI coding agent: codex, claude, agy +ready +``` + +**쉽게 말하면** + +- 릴리스를 처음 상태(active 1.0.0, 카나리 없음)로 돌리고, 비교용 v1 · v2(`rel-v1` · `rel-v2`)를 게시하고, `doctor`가 `ready`입니다. +- v2는 처음에 `gpt-4.1-nano`였는데 OpenAI가 지원 종료해 500이 났습니다. 10/27 `gpt-5.4-nano`로 바꾼 뒤의 기록입니다 (1단계 강의 2절). + +## Lab 01 · 무엇이 바뀌었나, 답마다 버전, 지시문은 저장소로 + +### 01 · Lab 01 예제 1 · v1 → v2에서 바뀐 것 → 7개 + +실행한 명령: + +```bash +uv run tools/flow.py diff reference/helpdesk-v1.json reference/helpdesk-v2.json +uv run tools/flow.py diff reference/helpdesk-v1.json reference/helpdesk-v2.json --full +``` + +출력 (그대로): + +```text +74 IT helpdesk v1 (provided) → 74 IT helpdesk v2 (provided) ++ node 'Review check' (code) +~ 'Answer' parameters.options.systemMessage: changed (307 → 419 chars) +~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano +~ 'Question webhook' parameters.path: rel-v1 → rel-v2 ++ link Answer → Review check (main) ++ link Review check → Build result (main) +- link Answer → Build result (main) +7 change(s) + +74 IT helpdesk v1 (provided) → 74 IT helpdesk v2 (provided) ++ node 'Review check' (code) +~ 'Answer' parameters.options.systemMessage: changed (307 → 419 chars) + before: You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say "찾지 못했습니다" and that a staff member will help. Use 합니다체. + after: You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say "찾지 못했습니다" and that a staff member will help. Use 합니다체. +When the answer comes from a FAQ entry, end with its id in brackets, for example [FAQ-003]. Keep answers short. +~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano +~ 'Question webhook' parameters.path: rel-v1 → rel-v2 ++ link Answer → Review check (main) ++ link Review check → Build result (main) +- link Answer → Build result (main) +7 change(s) +``` + +**쉽게 말하면** + +- 노드 단위로 비교하니 바뀐 것이 7개: 새 노드 `Review check`, 지시문 307 → 419자, 모델 mini → nano, 웹훅 경로, 연결 셋. +- `--full`로 보면 지시문에 두 문장(출처 번호 붙이기 · 짧게)이 더해졌습니다. +- 셋(모델 · 지시문 · 흐름)이 **한꺼번에** 바뀌었다는 것이 핵심입니다. 예상한 결과. + +### 02 · 예제 1 · 같은 질문을 v1 · v2에 + +실행한 명령: + +```bash +uv run tools/flow.py call rel-v1 requests/faq-vpn.json -v +uv run tools/flow.py call rel-v2 requests/faq-vpn.json -v +``` + +출력 (그대로): + +```text +HTTP 200 · 2.8s + → request POST /webhook/rel-v1 + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ] + } + +HTTP 200 · 1.0s + → request POST /webhook/rel-v2 + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ] + } +``` + +**쉽게 말하면** + +- v1은 2.8초, 출처 번호 없음. v2는 1.0초, 끝에 `[FAQ-003]`. +- 답이 달라진 것은 보이지만, 이 한 번으로는 지시문 · 모델 · 노드 중 무엇 때문인지 모릅니다. 시간도 한 번 잰 값입니다 (예제 3에서 다시). + +### 03 · Lab 01 예제 2 · 답마다 version → `lab01` 3/3 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `reference/lab01-example2-version.json`): + +```text +my-work/helpdesk.json (v1 을 복사한 것) 의 웹훅 경로를 my-helpdesk 로, 이름을 "74 IT helpdesk (my)" 로 바꿔 줘. +Has question? 뒤에 Release info(Set) 를 넣어 release 1.0.0, prompt_version p1, model gpt-4.1-mini. +Answer model 의 모델 값은 ={{ $('Release info').first().json.model }} 로 Release info 의 model 을 읽게. +Build result 결과에 version : {release, prompt_version, model} 을 Release info 에서 더해 줘. 지시문은 그대로 . +기대 결과 cases/lab01.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab01-example2-version.json +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + release API (reference)' (w2caac7c1a6981b8): it used the same webhook path +✔ pushed '74 IT helpdesk + version (reference)' (id w473a152b356a298) + POST http://localhost:5678/webhook/my-helpdesk + +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 버전이 결과에 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 접수 현황도 버전과 함께 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/3 +report: runs/lab01-20261027-183702.json +``` + +**쉽게 말하면** + +- 답에 `version: {release 1.0.0, prompt_version p1, model gpt-4.1-mini}`가 붙었습니다. 모델 노드와 결과가 같은 `Release info`를 읽습니다. +- 빈 질문은 그대로 400. 3/3, 예상한 결과. + +### 04 · Lab 01 예제 3 · `lab01-compare`를 v1에 3번 → 9/15 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-compare.json --webhook rel-v1 --repeat 3 +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/rel-v1 · 5 case(s) × 3 +✔ VPN · 해결 낱말 #1 · 1.2s +✔ VPN · 해결 낱말 #2 · 1.2s +✔ VPN · 해결 낱말 #3 · 1.1s +✘ VPN · 출처 번호 #1 · 1.1s · answer has none of ['[FAQ-003]'] +✘ VPN · 출처 번호 #2 · 1.2s · answer has none of ['[FAQ-003]'] +✘ VPN · 출처 번호 #3 · 1.3s · answer has none of ['[FAQ-003]'] +✔ 프린터 · 해결 낱말 #1 · 1.0s +✔ 프린터 · 해결 낱말 #2 · 1.1s +✔ 프린터 · 해결 낱말 #3 · 1.1s +✘ 프린터 · 출처 번호 #1 · 1.1s · answer has none of ['[FAQ-'] +✘ 프린터 · 출처 번호 #2 · 1.2s · answer has none of ['[FAQ-'] +✘ 프린터 · 출처 번호 #3 · 1.0s · answer has none of ['[FAQ-'] +✔ 접수 현황 #1 · 0.9s +✔ 접수 현황 #2 · 1.0s +✔ 접수 현황 #3 · 1.1s +Passed 9/15 +report: runs/lab01-compare-20261027-183719.json +``` + +**쉽게 말하면** + +- 출처 번호 두 케이스가 **세 번 모두** ✘, 나머지 셋은 세 번 모두 ✔ (9/15). +- 흔들림이 아니라 늘 같은 결과입니다. v1은 출처 번호를 붙이지 않는 버전이라 ✘가 **예상한 것**입니다. + +### 05 · 예제 3 · 같은 테스트를 v2에 3번 → 15/15 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-compare.json --webhook rel-v2 --repeat 3 +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/rel-v2 · 5 case(s) × 3 +✔ VPN · 해결 낱말 #1 · 1.0s +✔ VPN · 해결 낱말 #2 · 1.0s +✔ VPN · 해결 낱말 #3 · 1.0s +✔ VPN · 출처 번호 #1 · 0.9s +✔ VPN · 출처 번호 #2 · 1.1s +✔ VPN · 출처 번호 #3 · 0.9s +✔ 프린터 · 해결 낱말 #1 · 1.4s +✔ 프린터 · 해결 낱말 #2 · 1.1s +✔ 프린터 · 해결 낱말 #3 · 1.2s +✔ 프린터 · 출처 번호 #1 · 1.4s +✔ 프린터 · 출처 번호 #2 · 1.1s +✔ 프린터 · 출처 번호 #3 · 1.2s +✔ 접수 현황 #1 · 1.1s +✔ 접수 현황 #2 · 1.0s +✔ 접수 현황 #3 · 1.0s +Passed 15/15 +report: runs/lab01-compare-20261027-183736.json +``` + +**쉽게 말하면** + +- v2는 다섯 케이스 모두 세 번 다 ✔. 출처 번호의 차이는 **흔들림이 아니라 바뀐 것**(신호)입니다. +- 그래도 지시문 · 모델 · 노드 중 무엇 때문인지는 아직 모릅니다 → Lab 02의 한 가지씩 바꾼 릴리스. + +### 06 · 예제 3 · v1 · v2 시간 (`bench -n 5`) + +실행한 명령: + +```bash +uv run tools/flow.py bench rel-v1 requests/faq-vpn.json -n 5 +uv run tools/flow.py bench rel-v2 requests/faq-vpn.json -n 5 +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/rel-v1 × 5 +✔ #1 · HTTP 200 · 1.26s · route=자동 답변 +✔ #2 · HTTP 200 · 1.11s · route=자동 답변 +✔ #3 · HTTP 200 · 1.23s · route=자동 답변 +✔ #4 · HTTP 200 · 1.34s · route=자동 답변 +✔ #5 · HTTP 200 · 1.18s · route=자동 답변 +p50 1.23s · p95 1.34s · max 1.34s · failures 0/5 +report: runs/bench-rel-v1-20261027-183742.json + +POST http://localhost:5678/webhook/rel-v2 × 5 +✔ #1 · HTTP 200 · 1.20s · route=자동 답변 +✔ #2 · HTTP 200 · 0.97s · route=자동 답변 +✔ #3 · HTTP 200 · 0.98s · route=자동 답변 +✔ #4 · HTTP 200 · 1.02s · route=자동 답변 +✔ #5 · HTTP 200 · 0.98s · route=자동 답변 +p50 0.98s · p95 1.20s · max 1.20s · failures 0/5 +report: runs/bench-rel-v2-20261027-183747.json +``` + +**쉽게 말하면** + +- v1 p50 1.23초 · p95 1.34초, v2 p50 0.98초 · p95 1.20초. v2가 조금 빠릅니다. +- 모델이 작아진 덕일 것 같지만 셋이 함께 바뀌어 단정할 수 없습니다. 시간은 실행마다 다릅니다. + +### 07 · Lab 01 과제 1 하기 전 → `lab01-challenge` 2/3 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✘ 지시문은 저장소에서 · 1.2s · version.prompt_source = null (expected registry) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +✔ 접수 현황 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini" + } + } +Passed 2/3 +report: runs/lab01-challenge-20261027-183751.json +``` + +**쉽게 말하면** + +- 첫 케이스 ✘: `version.prompt_source`가 없습니다. 지시문이 아직 워크플로우 안에 있기 때문입니다. +- 나머지 둘(p1은 출처 번호 없음, 접수 현황)은 ✔. 예상한 결과. + +### 08 · Lab 01 과제 1 · 지시문을 저장소에서 → 3/3 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab01-challenge-registry.json`): + +```text +my-work/helpdesk.json 의 Release info 뒤에 Get prompt(HTTP GET) 를 넣어 줘. +주소는 http://data-api:8800/prompts/helpdesk/ prompt_version (Release info 의 값을 표현식으로). +Answer 의 systemMessage 는 지금 글자 대신 Get prompt 의 text 를 표현식으로 읽게. +Build result 의 version 에 prompt_source : ' registry ' 를 더해 줘. 다른 것은 그대로. +기대 결과 cases/lab01-challenge.json, cases/lab01.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/74-lab01-challenge-registry.json +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + version (reference)' (w473a152b356a298): it used the same webhook path +✔ pushed '74 IT helpdesk + prompt registry (answer)' (id w10c91b5c7439d37) + POST http://localhost:5678/webhook/my-helpdesk + +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 3/3 +report: runs/lab01-challenge-20261027-183810.json +``` + +**쉽게 말하면** + +- `Get prompt`가 `/prompts/helpdesk/p1`을 읽고 Agent의 지시문이 그 `text`가 됐습니다. `prompt_source: registry`. +- 3/3, 예상한 결과. + +### 09 · 과제 1 뒤 `lab01` → 3/3 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 버전이 결과에 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황도 버전과 함께 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/3 +report: runs/lab01-20261027-183812.json +``` + +**쉽게 말하면** + +- 예제 2의 테스트도 그대로 통과. 지시문을 밖으로 꺼내도 답은 같습니다 (p1 글이 같으니까). + +### 10 · Lab 01 과제 2 하기 전 → `lab01-challenge2` 2/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge2.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✘ 저장소 고장 · 기본 지시문으로 답 · 0.1s · status 500 (expected 200); route = null (expected 자동 답변); version.prompt_source = null (expected fallback); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "slow", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "slow", + "slow_seconds": 8.0 + } +✘ 저장소 느림 · 오래 기다리지 않음 · 9.1s · took 9.1s (expected at most 6s); version.prompt_source = registry (expected fallback) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 되돌린 뒤 · 다시 저장소에서 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 2/4 +report: runs/lab01-challenge2-20261027-183824.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +``` + +**쉽게 말하면** + +- 저장소 고장(`prompts error`): **HTTP 500** `Error in workflow` (0.1초). 지시문 하나 못 가져와서 답을 통째로 못 했습니다. +- 저장소 느림(`prompts slow` 8초): **9.1초** 기다린 뒤 답 (`max_seconds 6` ✘). 답은 맞았지만 너무 오래 걸렸습니다. +- 평소 · 되돌린 뒤 두 케이스는 ✔. `after`가 고장 스위치를 매번 `ok`로 되돌렸습니다. 예상한 결과. + +### 11 · Lab 01 과제 2 · 저장소가 멈추면 기본 지시문 → 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab01-challenge2-fallback.json`): + +```text +my-work/helpdesk.json 의 Get prompt 에 options.timeout 2000 과 onError continueErrorOutput 를 넣어 줘. +성공 쪽은 Registry prompt(Set, text = $json.text), 실패 쪽은 Fallback prompt(Set, text = data-api/data/prompts/helpdesk- p1 .md 와 같은 글). +두 Set 모두 Search FAQ 로 이어 줘. Answer 의 systemMessage 는 Registry prompt 가 실행됐으면 그 text, 아니면 Fallback prompt 의 text. +version.prompt_source 는 Registry prompt 가 실행됐으면 'registry', 아니면 ' fallback '. +기대 결과 cases/lab01-challenge2.json, cases/lab01-challenge.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/74-lab01-challenge2-fallback.json +uv run tools/flow.py test cases/lab01-challenge2.json -v +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + prompt registry (answer)' (w10c91b5c7439d37): it used the same webhook path +✔ pushed '74 IT helpdesk + prompt fallback (answer)' (id w5215c7f75644ed0) + POST http://localhost:5678/webhook/my-helpdesk + +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 기본 지시문으로 답 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "slow", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "slow", + "slow_seconds": 8.0 + } +✔ 저장소 느림 · 오래 기다리지 않음 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 되돌린 뒤 · 다시 저장소에서 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 4/4 +report: runs/lab01-challenge2-20261027-183845.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +``` + +**쉽게 말하면** + +- 고장: 200, `fallback`, "인증서" (1.3초). 느림: 2초에 포기하고 기본 지시문으로 3.0초. +- 고장이 끝나면 다시 `registry`. 4/4, 예상한 결과. + +### 12 · 과제 2 뒤 `lab01-challenge` → 3/3 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ p1은 출처 번호를 붙이지 않음 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 접수 현황 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 있으시면 연락 주시기 바랍니다.", + "sources": [], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +Passed 3/3 +report: runs/lab01-challenge-20261027-183849.json +``` + +**쉽게 말하면** + +- 평소에는 여전히 저장소 지시문(`registry`)으로 답합니다. 기본 지시문 길을 더해도 평소 동작은 그대로. + +## Lab 02 · 릴리스 고르기, 후보 시험, 배포 전 게이트 + +### 13 · Lab 02 예제 1 하기 전 → `lab02` 1/5 + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate promote 6/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate rollback 0/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ 운영 중인 릴리스 (active) · 1.2s · version.channel = null (expected stable) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 후보 시험 · 1.1.0 고정 · 1.6s · version.release = 1.0.0 (expected 1.1.0); version.channel = null (expected pinned); version.prompt_version = p1 (expected p2); answer has none of ['[FAQ-003]'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다만, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 후보 시험 · 2.0.0 (작은 모델) · 1.3s · version.release = 1.0.0 (expected 2.0.0); version.model = gpt-4.1-mini (expected gpt-5.4-nano) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✘ 없는 릴리스 · 1.2s · status 200 (expected 400); known has none of ['1.0.0'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "prompt_source": "registry" + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/5 +report: runs/lab02-20261027-183854.json +``` + +**쉽게 말하면** + +- `release helpdesk`: active 1.0.0, 릴리스 다섯 개. +- 고정한 1.1.0 · 2.0.0도 1.0.0으로 답했고, 9.9.9도 200으로 답했습니다. 릴리스가 아직 워크플로우 안의 Set이라 본문 `release`를 보지 않습니다. `channel`도 없습니다. +- 빈 질문 하나만 ✔ (1/5). 예상한 결과. + +### 14 · Lab 02 예제 1 · 릴리스를 밖에서 읽고 고정 → 5/5 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `reference/lab02-example1-release.json`): + +```text +my-work/helpdesk.json 의 Release info(Set) 를 Get release(HTTP GET http://data-api:8800/releases/helpdesk) → Pick release(Code) → Known release?(If) 로 바꿔 줘. +Pick release: 본문 release 가 있으면 그 릴리스 · channel pinned , 없으면 active · channel stable . 고른 릴리스의 prompt_version · model 을 돌려준다. +releases 에 없으면 Known release? false → Reject release(400, {error: 'unknown release', release, known }). +Get prompt · Answer model · Build result 는 Release info 대신 Pick release 를 읽게. 기본 지시문(Fallback prompt)은 그대로. +기대 결과 cases/lab02.json, cases/lab01-challenge.json, cases/lab01-challenge2.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab02-example1-release.json +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + prompt fallback (answer)' (w5215c7f75644ed0): it used the same webhook path +✔ pushed '74 IT helpdesk + release API (reference)' (id w2caac7c1a6981b8) + POST http://localhost:5678/webhook/my-helpdesk + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 후보 시험 · 1.1.0 고정 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속이 끊기는 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 찾지 못했습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.0.0", + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 없는 릴리스 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ 빈 질문 · 0.2s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 5/5 +report: runs/lab02-20261027-183911.json +``` + +**쉽게 말하면** + +- 본문 `release`가 없으면 1.0.0 · `stable`, 있으면 그 릴리스 · `pinned`. 2.0.0은 `gpt-5.4-nano`로 답했고, 9.9.9는 400 `known` 목록. +- 5/5, 예상한 결과. + +### 15 · 예제 1 뒤 `lab01-challenge` 3/3 · `lab01-challenge2` 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge.json +uv run tools/flow.py test cases/lab01-challenge2.json +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 3 case(s) × 1 +✔ 지시문은 저장소에서 · 1.2s +✔ p1은 출처 번호를 붙이지 않음 · 1.3s +✔ 접수 현황 · 1.2s +Passed 3/3 +report: runs/lab01-challenge-20261027-183915.json + +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.4s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 기본 지시문으로 답 · 1.1s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} +✔ 저장소 느림 · 오래 기다리지 않음 · 3.1s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 되돌린 뒤 · 다시 저장소에서 · 1.3s +Passed 4/4 +report: runs/lab01-challenge2-20261027-183922.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- Lab 01의 테스트도 그대로 통과. 릴리스를 밖에서 읽게 바꿔도 지시문 저장소 · 기본 지시문은 그대로 일합니다. + +### 16 · Lab 02 예제 2 · 약한 golden으로 네 릴리스 → 모두 PASS + +실행한 명령: + +```bash +uv run tools/flow.py gate cases/golden.json --set release=1.0.0 --max-p95 3 +uv run tools/flow.py gate cases/golden.json --set release=1.1.0 --max-p95 3 +uv run tools/flow.py gate cases/golden.json --set release=1.2.0 --max-p95 3 +uv run tools/flow.py gate cases/golden.json --set release=2.0.0 --max-p95 3 +uv run tools/flow.py call my-helpdesk '{"question": "프린터 출력이 안 돼요", "user": "최유진", "release": "1.2.0"}' -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.2s +✔ 비밀번호 · 1.2s +✔ 프린터 · 1.1s +✔ 메일함 · 1.3s +✔ 접수 처리 중 · 1.0s +✔ 접수 처리 완료 · 1.6s +Passed 6/6 +report: runs/golden-20261027-183929.json +gate [release=1.0.0]: pass 6/6 (100%) · p95 1.6s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.0s +✔ 프린터 · 1.1s +✔ 메일함 · 1.1s +✔ 접수 처리 중 · 1.1s +✔ 접수 처리 완료 · 1.1s +Passed 6/6 +report: runs/golden-20261027-183936.json +gate [release=1.1.0]: pass 6/6 (100%) · p95 1.1s → PASS +✔ gate record → /gates/helpdesk: 1.1.0 promote + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 0.8s +✔ 비밀번호 · 0.9s +✔ 프린터 · 0.8s +✔ 메일함 · 1.0s +✔ 접수 처리 중 · 1.2s +✔ 접수 처리 완료 · 1.5s +Passed 6/6 +report: runs/golden-20261027-183942.json +gate [release=1.2.0]: pass 6/6 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 1.2.0 promote + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.1s +✔ 프린터 · 2.0s +✔ 메일함 · 1.2s +✔ 접수 처리 중 · 1.0s +✔ 접수 처리 완료 · 1.2s +Passed 6/6 +report: runs/golden-20261027-183950.json +gate [release=2.0.0]: pass 6/6 (100%) · p95 2.0s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote + +HTTP 200 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.2.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +``` + +**쉽게 말하면** + +- 1.0.0 · 1.1.0 · 1.2.0 · 2.0.0 **모두 6/6 PASS** (p95 1.6 · 1.1 · 1.5 · 2.0초). +- 그런데 1.2.0의 프린터 답은 "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요." 해결 방법이 없는 떠넘기기입니다. +- golden이 `status 200`과 `route`만 봐서 생긴 일입니다. **게이트는 테스트만큼만 막습니다.** 예상한 결과 (일부러 보여 주는 것). + +### 17 · Lab 02 예제 3 · 기준선: 시간과 3번 반복 + +실행한 명령: + +```bash +uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 --set release=1.0.0 +uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 --set release=2.0.0 +uv run tools/flow.py gate cases/golden.json --set release=1.0.0 --repeat 3 --max-p95 3 +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.18s · route=자동 답변, release=1.0.0 +✔ #2 · HTTP 200 · 1.42s · route=자동 답변, release=1.0.0 +✔ #3 · HTTP 200 · 1.17s · route=자동 답변, release=1.0.0 +✔ #4 · HTTP 200 · 1.23s · route=자동 답변, release=1.0.0 +✔ #5 · HTTP 200 · 1.23s · route=자동 답변, release=1.0.0 +p50 1.23s · p95 1.42s · max 1.42s · failures 0/5 +report: runs/bench-my-helpdesk-20261027-183957.json + +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.10s · route=자동 답변, release=2.0.0 +✔ #2 · HTTP 200 · 1.24s · route=자동 답변, release=2.0.0 +✔ #3 · HTTP 200 · 1.05s · route=자동 답변, release=2.0.0 +✔ #4 · HTTP 200 · 1.12s · route=자동 답변, release=2.0.0 +✔ #5 · HTTP 200 · 1.12s · route=자동 답변, release=2.0.0 +p50 1.12s · p95 1.24s · max 1.24s · failures 0/5 +report: runs/bench-my-helpdesk-20261027-184003.json + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 3 +✔ VPN #1 · 1.1s +✔ VPN #2 · 1.1s +✔ VPN #3 · 1.2s +✔ 비밀번호 #1 · 1.1s +✔ 비밀번호 #2 · 1.3s +✔ 비밀번호 #3 · 1.3s +✔ 프린터 #1 · 1.1s +✔ 프린터 #2 · 1.0s +✔ 프린터 #3 · 1.3s +✔ 메일함 #1 · 1.1s +✔ 메일함 #2 · 1.1s +✔ 메일함 #3 · 1.1s +✔ 접수 처리 중 #1 · 1.1s +✔ 접수 처리 중 #2 · 1.0s +✔ 접수 처리 중 #3 · 1.0s +✔ 접수 처리 완료 #1 · 1.5s +✔ 접수 처리 완료 #2 · 1.1s +✔ 접수 처리 완료 #3 · 1.1s +Passed 18/18 +report: runs/golden-20261027-184024.json +gate [release=1.0.0]: pass 18/18 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote +``` + +**쉽게 말하면** + +- VPN p50 · p95: 1.0.0 1.23 · 1.42초, 2.0.0 1.12 · 1.24초. 지시문이 같은(1.1.0과) 상태에서 모델만 바뀐 2.0.0이 조금 빠릅니다. +- 1.0.0 golden 3번 반복 18/18 (p95 1.5초). 흔들리지 않으니 이 게이트의 FAIL은 믿을 수 있습니다. `--max-p95 3`은 약 두 배 여유. + +### 18 · Lab 02 과제 1 · 강한 golden → 1.0.0 · 1.1.0 PASS, 1.2.0 FAIL + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `cases/my-golden.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab02-challenge-golden.json`): + +```text +cases/golden.json 을 cases/my-golden.json 으로 복사해 expect 만 강하게 바꿔 줘. 워크플로우는 바꾸지 않는다 . +질문마다 답에 꼭 있어야 할 낱말을 contains 로 (아래는 내가 FAQ · 접수 건 데이터에서 정한 것): +VPN 인증서 / VPN 업데이트 · 최신 / 비밀번호 비밀번호 찾기 · 휴대폰 / 프린터 대기열 · 전원 / 메일함 보관함 · 삭제 / +T-1002 처리 중 / T-1002 10월 20일 · 20일 / T-1001 처리 완료 · 완료. +프린터 질문에는 " IT 포털을 확인 · 포털을 참고 " 같은 떠넘기기 말이 없어야 한다 (not_contains). +기대: gate --set release=1.0.0 · 1.1.0 PASS, release= 1.2.0 FAIL. +``` + +실행한 명령: + +```bash +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.0.0 --max-p95 3 +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.1.0 --max-p95 3 +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.2.0 --max-p95 3 +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.0.0 --max-p95 3 +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.2s +✔ VPN · 순서도 (업데이트) · 1.3s +✔ 비밀번호 · 해결 방법 · 1.4s +✔ 프린터 · 해결 방법 · 1.1s +✔ 메일함 · 해결 방법 · 1.2s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.1s +✔ 포털로 떠넘기지 않기 · 1.1s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184034.json +gate [release=1.0.0]: pass 9/9 (100%) · p95 1.4s → PASS +✔ gate record → /gates/helpdesk: 1.0.0 promote + +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.0s +✔ VPN · 순서도 (업데이트) · 1.7s +✔ 비밀번호 · 해결 방법 · 1.1s +✔ 프린터 · 해결 방법 · 1.3s +✔ 메일함 · 해결 방법 · 1.0s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.0s +✔ 포털로 떠넘기지 않기 · 1.2s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184045.json +gate [release=1.1.0]: pass 9/9 (100%) · p95 1.7s → PASS +✔ gate record → /gates/helpdesk: 1.1.0 promote + +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✘ VPN · 해결 방법 · 0.9s · answer has none of ['인증서'] +✘ VPN · 순서도 (업데이트) · 0.7s · answer has none of ['업데이트', '최신'] +✘ 비밀번호 · 해결 방법 · 0.9s · answer has none of ['비밀번호 찾기', '휴대폰'] +✘ 프린터 · 해결 방법 · 0.9s · answer has none of ['대기열', '전원'] +✘ 메일함 · 해결 방법 · 0.9s · answer has none of ['보관함', '삭제'] +✘ 접수 처리 중 · 상태와 예정일 · 1.0s · answer has none of ['처리 중'] +✘ 접수 처리 중 · 예정일 · 0.9s · answer has none of ['10월 20일', '20일'] +✘ 접수 처리 완료 · 1.3s · answer has none of ['처리 완료', '완료'] +✔ 포털로 떠넘기지 않기 · 0.7s +Passed 1/9 +report: runs/74-lab02-challenge-golden-20261027-184053.json +gate [release=1.2.0]: pass 1/9 (11%) · p95 1.3s → FAIL · pass rate 11% < 100% +✔ gate record → /gates/helpdesk: 1.2.0 rollback + +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.1s +✔ VPN · 순서도 (업데이트) · 1.3s +✔ 비밀번호 · 해결 방법 · 1.3s +✔ 프린터 · 해결 방법 · 1.3s +✔ 메일함 · 해결 방법 · 1.3s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.1s +✔ 접수 처리 완료 · 1.2s +✔ 포털로 떠넘기지 않기 · 1.1s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184104.json +gate [release=2.0.0]: pass 9/9 (100%) · p95 1.3s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote +``` + +**쉽게 말하면** + +- 1.0.0 9/9 · 1.1.0 9/9 PASS, **1.2.0 1/9 FAIL**, 2.0.0 9/9 PASS (p95 1.3초). +- 1.2.0이 통과한 하나는 "포털로 떠넘기지 않기"(`not_contains`)입니다. 답이 "IT 포털**에서** … 확인해 주세요"라 "IT 포털을 확인" · "포털을 참고"에 걸리지 않았습니다. 막은 것은 해결 낱말(`contains`) 여덟 케이스. +- 테스트만 고쳤고 워크플로우는 그대로입니다. 예상한 결과. + +### 19 · Lab 02 과제 2 하기 전 → `lab02-challenge2` 4/5, 2.1.0 게이트 PASS + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02-challenge2.json -v +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.1.0 --max-p95 3 +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ 망가진 릴리스 2.1.0 · 400 release broken · 1.3s · status 200 (expected 400); error = null (expected release broken); prompt_version = null (expected p9) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "2.1.0", + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ 평소 · 1.1.0은 저장소의 p2 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 운영(active 1.0.0)은 그대로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 4/5 +report: runs/lab02-challenge2-20261027-184109.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ VPN · 해결 방법 · 1.2s +✔ VPN · 순서도 (업데이트) · 1.2s +✔ 비밀번호 · 해결 방법 · 1.5s +✔ 프린터 · 해결 방법 · 1.1s +✔ 메일함 · 해결 방법 · 1.3s +✔ 접수 처리 중 · 상태와 예정일 · 1.0s +✔ 접수 처리 중 · 예정일 · 1.0s +✔ 접수 처리 완료 · 1.0s +✔ 포털로 떠넘기지 않기 · 1.0s +Passed 9/9 +report: runs/74-lab02-challenge-golden-20261027-184120.json +gate [release=2.1.0]: pass 9/9 (100%) · p95 1.5s → PASS +✔ gate record → /gates/helpdesk: 2.1.0 promote +``` + +**쉽게 말하면** + +- 2.1.0(p9) 케이스 ✘: 400이어야 하는데 **200**으로 답했습니다. 저장소가 404를 줬지만 Lab 01 과제 2의 기본 지시문이 받아 버렸습니다. +- 그래서 강한 golden으로 2.1.0을 게이트하면 **9/9 PASS**. 망가진 릴리스가 게이트를 통과합니다 (위험!). +- 나머지 넷(평소 1.1.0, 저장소 고장 1.1.0 → fallback, 고장 중 없는 릴리스 400, 운영 1.0.0)은 ✔. 예상한 결과. + +### 20 · Lab 02 과제 2 · 404는 400 release broken → 5/5, 2.1.0 게이트 FAIL + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab02-challenge2-broken.json`): + +```text +my-work/helpdesk.json 의 Get prompt 에 options.response.response 로 neverError true, fullResponse true 를 더해 줘 (timeout 2000 · onError 는 그대로). +성공 쪽 Registry prompt 를 Code 로 바꿔: statusCode 200 이면 {state: 'registry', text: body.text}, 404 이면 {state: 'broken'}, 그 밖이면 {state: 'fallback', text: 기본 지시문}. +Prompt broken?(If, state == 'broken') true → Reject broken(HTTP 400 , {error: 'release broken', release, prompt_version}), false → Search FAQ. +prompt_source 는 Registry prompt 의 state 가 'registry' 면 registry, 아니면 fallback. 실패 쪽(시간 초과) Fallback prompt 는 그대로 둔다 . +기대 결과 cases/lab02-challenge2.json, cases/lab02.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/74-lab02-challenge2-broken.json +uv run tools/flow.py test cases/lab02-challenge2.json -v +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=2.1.0 --max-p95 3 +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + release API (reference)' (w2caac7c1a6981b8): it used the same webhook path +✔ pushed '74 IT helpdesk + broken release check (answer)' (id w86d00ad9781296e) + POST http://localhost:5678/webhook/my-helpdesk + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 망가진 릴리스 2.1.0 · 400 release broken · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "2.1.0" + } + ← response HTTP 400 + { + "error": "release broken", + "release": "2.1.0", + "prompt_version": "p9" + } +✔ 평소 · 1.1.0은 저장소의 p2 · 2.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "1.1.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "fallback" + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "error" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "error", + "slow_seconds": 8.0 + } +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "release": "9.9.9" + } + ← response HTTP 400 + { + "error": "unknown release", + "release": "9.9.9", + "known": [ + "1.0.0", + "1.1.0", + "1.2.0", + "2.0.0", + "2.1.0" + ] + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok" + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ 운영(active 1.0.0)은 그대로 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 5/5 +report: runs/lab02-challenge2-20261027-184139.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} + → request POST http://localhost:8800/admin/fault + { + "prompts": "ok", + "slow_seconds": 8 + } + ← response HTTP 200 + { + "faq": "ok", + "tickets": "ok", + "prompts": "ok", + "slow_seconds": 8.0 + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✘ VPN · 해결 방법 · 0.1s · status 400 (expected 200); route = null (expected 자동 답변); answer has none of ['인증서'] +✘ VPN · 순서도 (업데이트) · 0.1s · status 400 (expected 200); answer has none of ['업데이트', '최신'] +✘ 비밀번호 · 해결 방법 · 0.1s · status 400 (expected 200); answer has none of ['비밀번호 찾기', '휴대폰'] +✘ 프린터 · 해결 방법 · 0.0s · status 400 (expected 200); answer has none of ['대기열', '전원'] +✘ 메일함 · 해결 방법 · 0.1s · status 400 (expected 200); answer has none of ['보관함', '삭제'] +✘ 접수 처리 중 · 상태와 예정일 · 0.1s · status 400 (expected 200); answer has none of ['처리 중'] +✘ 접수 처리 중 · 예정일 · 0.1s · status 400 (expected 200); answer has none of ['10월 20일', '20일'] +✘ 접수 처리 완료 · 0.0s · status 400 (expected 200); answer has none of ['처리 완료', '완료'] +✘ 포털로 떠넘기지 않기 · 0.0s · status 400 (expected 200) +Passed 0/9 +report: runs/74-lab02-challenge-golden-20261027-184140.json +gate [release=2.1.0]: pass 0/9 (0%) · p95 0.0s → FAIL · pass rate 0% < 100% +✔ gate record → /gates/helpdesk: 2.1.0 rollback +``` + +**쉽게 말하면** + +- 2.1.0은 400 `{error: release broken, release 2.1.0, prompt_version p9}`. 저장소 장애(500)는 여전히 기본 지시문. +- 같은 강한 golden으로 2.1.0을 게이트하면 **0/9 FAIL** (모두 400이라 p95는 0.0초로 나옴). 설정 오류를 크게 실패하게 하자 게이트가 잡았습니다. +- 5/5, 예상한 결과. + +### 21 · 과제 2 뒤 `lab02` 5/5 · `lab01-challenge2` 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02.json +uv run tools/flow.py test cases/lab01-challenge2.json +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 1.3s +✔ 후보 시험 · 1.1.0 고정 · 1.1s +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.2s +✔ 없는 릴리스 · 0.1s +✔ 빈 질문 · 0.1s +Passed 5/5 +report: runs/lab02-20261027-184144.json + +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 저장소에서 · 1.2s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 기본 지시문으로 답 · 1.2s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "slow", "slow_seconds": 8.0} +✔ 저장소 느림 · 오래 기다리지 않음 · 3.0s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 되돌린 뒤 · 다시 저장소에서 · 1.0s +Passed 4/4 +report: runs/lab01-challenge2-20261027-184150.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 앞의 테스트가 그대로 통과. 응답 코드로 가르는 길을 더해도 고정 · 없는 릴리스 · 기본 지시문은 그대로입니다. + +## Lab 03 · 배포 · 되돌리기, 카나리, 릴리스 점검 + +### 22 · Lab 03 예제 1 · 배포 · 되돌리기 → `lab03` 2/2 + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk 1.1.0 +uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +uv run tools/flow.py release helpdesk 1.0.0 +uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +uv run tools/flow.py test cases/lab03.json -v +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.1.0 · no canary + 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + * 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +HTTP 200 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +HTTP 200 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 2 case(s) × 1 +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.1.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.1.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 배포 · 1.1.0을 active로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.0.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 되돌리기 · 1.0.0 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 2/2 +report: runs/lab03-20261027-184155.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +``` + +**쉽게 말하면** + +- `release helpdesk 1.1.0` 뒤 다음 요청이 1.1.0 · `[FAQ-003]`, `release helpdesk 1.0.0` 뒤 다음 요청이 1.0.0 · 출처 번호 없음. +- push도 n8n 재시작도 없었습니다. 값 하나. 2/2, 예상한 결과. + +### 23 · Lab 03 예제 2 하기 전 → `lab03-canary` 3/6 + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk --canary 1.1.0 --percent 50 +uv run tools/flow.py test cases/lab03-canary.json -v +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.0.0 · canary 1.1.0 50% + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + c 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + canary users: 최유진(0), 정우성(42) + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.1.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.1.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.1.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✘ 최유진 (bucket 0) → 카나리 · 1.1s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✘ 정우성 (bucket 42) → 카나리 · 1.3s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 (bucket 56) → 운영 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 (bucket 80) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 고정이 카나리보다 먼저 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✘ 같은 사람은 늘 같은 쪽 · 1.2s · version.release = 1.0.0 (expected 1.1.0); version.channel = stable (expected canary) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 3/6 +report: runs/lab03-canary-20261027-184202.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +``` + +**쉽게 말하면** + +- 카나리 1.1.0 50%를 켰지만 최유진 · 정우성(카나리 대상)도 1.0.0 · `stable`로 답했습니다. 워크플로우가 아직 카나리를 보지 않습니다. +- 운영 쪽 둘과 "고정이 카나리보다 먼저"는 ✔ (3/6). 예상한 결과. + +### 24 · Lab 03 예제 2 · 카나리 → 6/6, `lab02` 5/5, `lab02-challenge2` 5/5 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example2-canary.json`): + +```text +my-work/helpdesk.json 의 Pick release 에 카나리를 더해 줘. +bucket = user 이름 글자의 codePointAt 합 % 100 . 본문 release 가 없고 canary 가 있고 bucket < canary.percent 이면 +canary.release 를 쓰고 channel ' canary '. 순서는 고정 > 카나리 > active ( 고정(본문 release) 가 맨 먼저). +기대 결과 cases/lab03-canary.json, cases/lab02.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab03-example2-canary.json +uv run tools/flow.py test cases/lab03-canary.json -v +uv run tools/flow.py test cases/lab02.json +uv run tools/flow.py test cases/lab02-challenge2.json +uv run tools/flow.py release helpdesk --reset +``` + +출력 (그대로): + +```text +! unpublished '74 IT helpdesk + broken release check (answer)' (w86d00ad9781296e): it used the same webhook path +✔ pushed '74 IT helpdesk + canary (reference)' (id w2e1e5fc96604a25) + POST http://localhost:5678/webhook/my-helpdesk + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.1.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.1.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.1.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 최유진 (bucket 0) → 카나리 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 정우성 (bucket 42) → 카나리 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하면 문의해 주세요. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 이준호 (bucket 56) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주세요.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 (bucket 80) → 운영 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "메일함 용량이 꽉 찼대요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-005" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 고정이 카나리보다 먼저 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "release": "1.0.0" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "pinned", + "prompt_source": "registry" + } + } +✔ 같은 사람은 늘 같은 쪽 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +Passed 6/6 +report: runs/lab03-canary-20261027-184226.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 운영 중인 릴리스 (active) · 1.6s +✔ 후보 시험 · 1.1.0 고정 · 1.3s +✔ 후보 시험 · 2.0.0 (작은 모델) · 1.4s +✔ 없는 릴리스 · 0.1s +✔ 빈 질문 · 0.1s +Passed 5/5 +report: runs/lab02-20261027-184231.json + +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ 망가진 릴리스 2.1.0 · 400 release broken · 0.1s +✔ 평소 · 1.1.0은 저장소의 p2 · 1.2s +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 1.1.0도 기본 지시문으로 · 1.2s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "error", "slow_seconds": 8.0} +✔ 저장소 고장 · 없는 릴리스는 그대로 400 · 0.0s +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ 운영(active 1.0.0)은 그대로 · 1.1s +Passed 5/5 +report: runs/lab02-challenge2-20261027-184235.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 +``` + +**쉽게 말하면** + +- 최유진(0) · 정우성(42)은 1.1.0 · `canary`, 이준호(56) · 김민지(80)는 1.0.0 · `stable`. 같은 사람은 다시 물어도 같은 쪽. 고정은 카나리보다 먼저. +- 앞의 테스트도 그대로. 끝에 `--reset`. 예상한 결과. + +### 25 · Lab 03 예제 3 · 나쁜 카나리(1.2.0 50%) → 4/6 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-watch.json -v +uv run tools/flow.py release helpdesk +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": {"release": "1.2.0", "percent": 50}, "require_gate": false, "releases" + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "1.2.0", + "percent": 50 + } + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": { + "release": "1.2.0", + "percent": 50 + }, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✘ 최유진 · 프린터 · 0.9s · answer has none of ['대기열'] + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✘ 정우성 · 프린터 · 0.9s · answer has none of ['대기열'] + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "IT 포털에서 프린터 문제 해결 방법을 확인해 주세요.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.2.0", + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "channel": "canary", + "prompt_source": "registry" + } + } +✔ 한지훈 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 박서연 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 · 프린터 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 4/6 +report: runs/lab03-watch-20261027-184241.json + +✔ helpdesk: active 1.0.0 · canary 1.2.0 50% + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + c 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + canary users: 최유진(0), 정우성(42) +``` + +**쉽게 말하면** + +- 카나리 둘(최유진 · 정우성)은 "대기열"이 없는 한 문장 답(1.2.0)이라 ✘, 운영 넷은 ✔. +- `release helpdesk`가 카나리 1.2.0 50%를 보여 줍니다. 전원 배포였다면 여섯 명 모두 ✘였을 것. 예상한 결과. + +### 26 · 예제 3 · `--no-canary` 뒤 → 6/6 + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk --no-canary +uv run tools/flow.py test cases/lab03-watch.json --no-before -v +uv run tools/flow.py release helpdesk --reset +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 최유진 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 정우성 · 프린터 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "정우성" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 접수된 문의 건은 없으며, 추가 도움이 필요하시면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 한지훈 · 프린터 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "한지훈" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 이준호 · 프린터 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 박서연 · 프린터 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ 김민지 · 프린터 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "김민지" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 6/6 +report: runs/lab03-watch-20261027-184249.json + +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 gate promote 9/9 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 9/9 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 1/9 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 9/9 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) gate rollback 0/9 +``` + +**쉽게 말하면** + +- 카나리를 끄고 `--no-before`로 다시 재니 여섯 명 모두 ✔. active는 처음부터 1.0.0이라 운영 쪽은 아무 일도 없었습니다. + +### 27 · Lab 03 과제 1 · 릴리스 점검 → `lab03-challenge` 3/3 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/release-check.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab03-challenge-release-check.json`): + +```text +my-work/release-check.json 을 새로 만들어 줘. 웹훅 my-release-check , 본문 release 가 없으면 400. +Golden questions(Code): cases/my-golden.json 의 질문 6개(question · user · expect 낱말)를 항목 6개로, 각 항목에 release. +Ask helpdesk(HTTP POST http://localhost:5678/webhook/my-helpdesk): 항목마다 {question, user, release } 로 (neverError, timeout 30000). +Judge(Code): 답에 expect 낱말 중 하나가 있고 답의 version.release 가 점검한 release 와 같으면 이면 통과. +결과 {release, passed, total, failed, seconds, verdict}: 모두 통과면 ' promote ', 아니면 'rollback'. +기대 결과 cases/lab03-challenge.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/74-lab03-challenge-release-check.json +uv run tools/flow.py test cases/lab03-challenge.json -v +uv run tools/flow.py call my-release-check '{"release": "2.0.0"}' -v +``` + +출력 (그대로): + +```text +! unpublished '74 Release check + gate record (answer)' (w9589252e9498695): it used the same webhook path +✔ pushed '74 Release check (answer)' (id w50ae3687e1cc859) + POST http://localhost:5678/webhook/my-release-check + +POST http://localhost:5678/webhook/my-release-check · 3 case(s) × 1 +✔ 1.1.0 · 내보내도 됨 · 3.1s + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 2, + "verdict": "promote" + } +✔ 1.2.0 · 되돌림 · 1.6s + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 1.6, + "verdict": "rollback" + } +✔ 릴리스 없음 · 0.0s + → request POST /webhook/my-release-check + {} + ← response HTTP 400 + { + "error": "release is required" + } +Passed 3/3 +report: runs/lab03-challenge-20261027-184308.json + +HTTP 200 · 1.9s + → request POST /webhook/my-release-check + { + "release": "2.0.0" + } + ← response HTTP 200 + { + "release": "2.0.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 1.8, + "verdict": "promote" + } +``` + +**쉽게 말하면** + +- 1.1.0 → `promote` 6/6 (3.1초), 1.2.0 → `rollback` 0/6 (1.6초), `release` 없으면 400. +- `call my-release-check '{"release": "2.0.0"}'` → `promote` 6/6 (1.8초). 3/3, 예상한 결과. + +### 28 · Lab 03 과제 2 하기 전 → 점검 둘 ✘, 1.1.0 배포가 409로 멈춤 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-challenge2.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/gates/helpdesk/clear · HTTP 200 · 0.0s · {"cleared": 19} + → request POST http://localhost:8800/gates/helpdesk/clear + {} + ← response HTTP 200 + { + "cleared": 19 + } +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk/policy · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk/policy + { + "require_gate": true + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-release-check · 5 case(s) × 1 +✘ 1.2.0 점검 · 기록 · 24.2s · recorded = null (expected true) + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 24.1, + "verdict": "rollback" + } +✘ 1.1.0 점검 · 기록 · 7.9s · recorded = null (expected true) + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 7.9, + "verdict": "promote" + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.2.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.2.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.2.0", + "last_gate": null + } +✔ 점검에 떨어진 1.2.0은 배포 거부 (409) · 운영은 1.0.0 · 7.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 문의는 없으며, 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✘ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.1.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.1.0", + "last_gate": null + } +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✘ the 'before' step failed (http://localhost:8800/releases/helpdesk). Is it pushed / is the data API the day 74 version? +``` + +**쉽게 말하면** + +- 점검 두 케이스 ✘: 결과에 `recorded`가 없습니다 (판단은 맞게 냄). +- 1.2.0 배포 거부 케이스는 ✔ (409). 그런데 기록이 **아예 없어서** 거부된 것입니다. +- 다음 케이스의 `before`(1.1.0 배포)가 **409 `not gated` · `last_gate: null`**로 실패해 테스트가 멈췄습니다 (그래서 이 단계는 보고서 `.json`이 없음). 점검이 promote라고 말했지만 남기지 않았기 때문. 예상한 결과. +- 이번 실행에서 1.2.0 점검이 24.2초 걸렸습니다 (OpenAI 응답이 이때 느렸음, 다음 단계에서는 10.6초). + +### 29 · Lab 03 과제 2 · 점검 기록 · 지난 것만 배포 → 5/5 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/release-check.json` · 이 기록에서 push한 같은 역할의 정답 `answers/74-lab03-challenge2-gated.json`): + +```text +my-work/release-check.json 의 Judge 와 Send verdict 사이에 Record gate(HTTP POST http://data-api:8800/ gates/helpdesk ) 를 넣어 줘. +본문: {release, verdict, passed, total, failed, seconds, source: 'my-release-check'} (Judge 의 결과에서). +Record gate 는 timeout 3000, neverError, onError continueRegularOutput (기록이 실패해도 판단은 보냄). +Send verdict 는 Judge 의 결과에 recorded: (기록 응답의 stored 가 true 인지) 를 더해 보낸다. +워크플로우에서 /releases 에는 POST 하지 않는다 . 기대 결과 cases/lab03-challenge2.json, cases/lab03-challenge.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/74-lab03-challenge2-gated.json +uv run tools/flow.py test cases/lab03-challenge2.json -v +uv run tools/flow.py release helpdesk +``` + +출력 (그대로): + +```text +! unpublished '74 Release check (answer)' (w50ae3687e1cc859): it used the same webhook path +✔ pushed '74 Release check + gate record (answer)' (id w9589252e9498695) + POST http://localhost:5678/webhook/my-release-check + +✔ before: POST http://localhost:8800/gates/helpdesk/clear · HTTP 200 · 0.0s · {"cleared": 0} + → request POST http://localhost:8800/gates/helpdesk/clear + {} + ← response HTTP 200 + { + "cleared": 0 + } +✔ before: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ before: POST http://localhost:8800/releases/helpdesk/policy · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk/policy + { + "require_gate": true + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +POST http://localhost:5678/webhook/my-release-check · 5 case(s) × 1 +✔ 1.2.0 점검 · 기록 · 10.6s + → request POST /webhook/my-release-check + { + "release": "1.2.0" + } + ← response HTTP 200 + { + "release": "1.2.0", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 9.5, + "verdict": "rollback", + "recorded": true + } +✔ 1.1.0 점검 · 기록 · 1.9s + → request POST /webhook/my-release-check + { + "release": "1.1.0" + } + ← response HTTP 200 + { + "release": "1.1.0", + "passed": 6, + "total": 6, + "failed": [], + "seconds": 1.7, + "verdict": "promote", + "recorded": true + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "1.2.0", "last_gate": {"t": 1793094252.829, "release": "1.2.0", "verdict": "rollback", + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.2.0" + } + ← response HTTP 409 + { + "error": "not gated", + "release": "1.2.0", + "last_gate": { + "t": 1793094252.829, + "release": "1.2.0", + "verdict": "rollback", + "passed": 0, + "total": 6, + "failed": [ + "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "프린터 출력이 안 돼요", + "메일함 용량이 꽉 찼대요", + "T-1002 처리 상태 알려 주세요", + "T-1001 어떻게 됐나요?" + ], + "seconds": 9.5, + "source": "my-release-check" + } + } +✔ 점검에 떨어진 1.2.0은 배포 거부 (409) · 운영은 1.0.0 · 7.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 문의 건이 없어 추가 지원이 필요하시면 IT 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.0.0", + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.1.0", "canary": null, "require_gate": true, "releases": {"1.0.0": {"prompt_version": " + → request POST http://localhost:8800/releases/helpdesk + { + "active": "1.1.0" + } + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.1.0", + "canary": null, + "require_gate": true, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } +✔ 점검을 지난 1.1.0은 배포 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 문의해 주세요. [FAQ-003]", + "sources": [ + "FAQ-003" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 409 · 0.0s · {"error": "not gated", "release": "2.0.0", "last_gate": null} + → request POST http://localhost:8800/releases/helpdesk + { + "canary": { + "release": "2.0.0", + "percent": 50 + } + } + ← response HTTP 409 + { + "error": "not gated", + "release": "2.0.0", + "last_gate": null + } +✔ 점검 안 한 2.0.0은 카나리도 거부 (409) · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다. [FAQ-004]", + "sources": [ + "FAQ-004" + ], + "version": { + "release": "1.1.0", + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "channel": "stable", + "prompt_source": "registry" + } + } +Passed 5/5 +report: runs/lab03-challenge2-20261027-184425.json +✔ after: POST http://localhost:8800/releases/helpdesk/reset · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.0.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version": + → request POST http://localhost:8800/releases/helpdesk/reset + {} + ← response HTTP 200 + { + "name": "helpdesk", + "active": "1.0.0", + "canary": null, + "require_gate": false, + "releases": { + "1.0.0": { + "prompt_version": "p1", + "model": "gpt-4.1-mini", + "note": "첫 릴리스" + }, + "1.1.0": { + "prompt_version": "p2", + "model": "gpt-4.1-mini", + "note": "FAQ 답 끝에 출처 번호" + }, + "1.2.0": { + "prompt_version": "p3", + "model": "gpt-4.1-mini", + "note": "답을 한 문장으로 짧게" + }, + "2.0.0": { + "prompt_version": "p2", + "model": "gpt-5.4-nano", + "note": "더 작은 모델 (비용)" + }, + "2.1.0": { + "prompt_version": "p9", + "model": "gpt-4.1-mini", + "note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)" + } + } + } + +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) +``` + +**쉽게 말하면** + +- 1.2.0 점검 → `rollback` · `recorded` true, 1.1.0 → `promote` · `recorded` true. +- 정책이 켜진 상태에서 1.2.0 배포는 409(마지막 기록 rollback), 1.1.0 배포는 200 → 다음 답이 1.1.0 · `stable`, 점검 안 한 2.0.0 카나리는 409. +- 5/5. `after`가 reset해서 끝에 active 1.0.0 · 정책 꺼짐. 예상한 결과. + +### 30 · 정책을 켜고 `flow.py gate` 기록으로 배포 · doctor + +실행한 명령: + +```bash +uv run tools/flow.py release helpdesk --require-gate on +uv run tools/flow.py release helpdesk 2.1.0 +uv run tools/flow.py gate cases/golden.json --set release=2.0.0 --max-p95 3 +uv run tools/flow.py release helpdesk 2.0.0 +uv run tools/flow.py release helpdesk --reset +uv run tools/flow.py doctor +``` + +출력 (그대로): + +```text +✔ helpdesk: active 1.0.0 · no canary · deploy needs a passing gate + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +✘ refused: 2.1.0 has no passing gate record (never gated) + → run the gate (flow.py gate ... --set release=) or my-release-check first + +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ VPN · 1.1s +✔ 비밀번호 · 1.1s +✔ 프린터 · 1.3s +✔ 메일함 · 1.4s +✔ 접수 처리 중 · 1.1s +✔ 접수 처리 완료 · 1.1s +Passed 6/6 +report: runs/golden-20261027-184432.json +gate [release=2.0.0]: pass 6/6 (100%) · p95 1.4s → PASS +✔ gate record → /gates/helpdesk: 2.0.0 promote + +✔ helpdesk: active 2.0.0 · no canary · deploy needs a passing gate + 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + * 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 6/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +✔ helpdesk: active 1.0.0 · no canary + * 1.0.0 prompt p1 model gpt-4.1-mini 첫 릴리스 + 1.1.0 prompt p2 model gpt-4.1-mini FAQ 답 끝에 출처 번호 gate promote 6/6 + 1.2.0 prompt p3 model gpt-4.1-mini 답을 한 문장으로 짧게 gate rollback 0/6 + 2.0.0 prompt p2 model gpt-5.4-nano 더 작은 모델 (비용) gate promote 6/6 + 2.1.0 prompt p9 model gpt-4.1-mini 출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요) + +! n8n install folder /Users/mcchae/day65-n8n + → day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더> +✔ n8n container day65-n8n-n8n-1 +✔ n8n http://localhost:5678 +✔ data API http://localhost:8800 +✔ releases /releases/helpdesk · gates /gates/helpdesk (day 74 data API) +✔ n8n → http://data-api:8800 +✔ credential 'OpenAI (.env)' (openAiCourse0001) +✔ node catalog (for check) +✔ CLI coding agent: codex, claude, agy +ready +``` + +**쉽게 말하면** + +- 정책을 켠 뒤 2.1.0 배포: `refused … (never gated)`. +- `gate cases/golden.json --set release=2.0.0` PASS → `/gates/helpdesk`에 2.0.0 promote 기록 → `release helpdesk 2.0.0`이 됩니다. 노트북의 게이트 기록도 정책이 인정합니다. +- `--reset` 뒤 `doctor` → `ready`. 내일을 위해 처음 상태로 돌려 두었습니다. + +## 다시 돌려 보기 + +강사 노트북에서 같은 순서를 다시 돌리려면 (강사 저장소 `~/work/AIO-01D4/Last3Mon/74`, 정답 파일이 있는 곳): + +```bash +cd ~/work/AIO-01D4/Last3Mon/74 +uv run tools/flow.py release helpdesk --reset +uv run tools/flow.py push reference/helpdesk-v1.json reference/helpdesk-v2.json +uv run tools/flow.py push answers/74-lab02-challenge2-broken.json && uv run tools/flow.py test cases/lab02-challenge2.json -v +uv run tools/flow.py gate answers/74-lab02-challenge-golden.json --set release=1.2.0 --max-p95 3 # FAIL +uv run tools/flow.py push reference/lab03-example2-canary.json answers/74-lab03-challenge2-gated.json +uv run tools/flow.py test cases/lab03-canary.json -v +uv run tools/flow.py test cases/lab03-challenge2.json -v +uv run tools/flow.py release helpdesk --reset && uv run tools/flow.py doctor +``` + +- 결과의 ✔ · ✘(판정)는 같게 나와야 합니다. LLM 문장과 시간은 다를 수 있습니다. 특히 1.2.0 점검 · 게이트 시간은 OpenAI 응답에 따라 1.6초 ~ 24초까지 봤습니다. diff --git a/Last3Mon/74/answers/check-data-api.sh b/Last3Mon/74/answers/check-data-api.sh new file mode 100644 index 0000000..a67cc0b --- /dev/null +++ b/Last3Mon/74/answers/check-data-api.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Quick check of the day 74 data API (instructor, before class): releases, policy, gates, prompt fault switch. +set -u +B=localhost:8800 +show() { echo "\$ $1"; shift; "$@"; echo; } +show "GET /releases/helpdesk" curl -s $B/releases/helpdesk +show "policy on" curl -s -XPOST $B/releases/helpdesk/policy -d '{"require_gate":true}' +show "deploy 1.1.0 without a gate record (409)" curl -s -XPOST $B/releases/helpdesk -d '{"active":"1.1.0"}' +show "gate record 1.1.0 promote" curl -s -XPOST $B/gates/helpdesk -d '{"release":"1.1.0","verdict":"promote","passed":6,"total":6,"source":"check"}' +show "deploy 1.1.0 again (200)" curl -s -XPOST $B/releases/helpdesk -d '{"active":"1.1.0"}' +show "fault prompts error" curl -s -XPOST $B/admin/fault -d '{"prompts":"error"}' +show "GET /prompts/helpdesk/p1 (500)" curl -s $B/prompts/helpdesk/p1 +show "fault prompts ok" curl -s -XPOST $B/admin/fault -d '{"prompts":"ok"}' +show "GET /prompts/helpdesk/p9 (404)" curl -s $B/prompts/helpdesk/p9 +show "reset" curl -s -XPOST $B/releases/helpdesk/reset +show "gates clear" curl -s -XPOST $B/gates/helpdesk/clear +show "GET /audit (day 73 endpoint still there)" curl -s $B/audit diff --git a/Last3Mon/74/tver/Doc/IMP-PLAN.md b/Last3Mon/74/tver/Doc/IMP-PLAN.md new file mode 100644 index 0000000..28809bb --- /dev/null +++ b/Last3Mon/74/tver/Doc/IMP-PLAN.md @@ -0,0 +1,36 @@ +# tver IMP-PLAN + +## 엔진 1단계 · 정리하고 비교하기 (R1 · R2 · R3 · R4) +- [x] `workflow.clean`: id · position · webhookId · 내보내기 도장 빼기, 노드 이름순 · `dumps`는 키 정렬 +- [x] `diff.compare(old, new)` → `Change{op, node, category, key, before, after, lines}` +- [x] `diff.category`: Chat model의 model → model, systemMessage · text · jsCode → prompt, 나머지 → flow +- [x] 80자 넘거나 여러 줄이면 `difflib` 줄 diff +- [x] tests: v1 → v2 7개 · 1/1/5, 위치 · id · 순서만 바꾼 파일 0개, systemMessage에 `+ … [FAQ-003]` +- [x] cli: `tver diff a.json b.json` +- 확인: `uv run pytest -q`, `uv run tver diff tests/data/helpdesk-v1.json tests/data/helpdesk-v2.json` + +## 엔진 2단계 · git에 저장 · 목록 (R5 · R6) +- [x] `Store.save_workflow`: 정리한 글이 같으면 "no change", 다르면 쓰고 커밋 (메시지 = 바로 전과의 요약) +- [x] `Store.log` · `Store.show`, `tver diff ` +- [x] tests: 두 번 저장 → 버전 1개, v2로 바꿔 저장 → 메시지에 "model 1 · prompt 1 · flow 5" +- 확인: `uv run tver --repo /tmp/s save tests/data/helpdesk-v1.json` 두 번, `log` + +## TUI 1단계 · 버전 목록 · 바뀐 것 트리 · 자세히 (R8) +- [x] 왼쪽 ListView(버전), 오른쪽 Tree(노드 → 바뀐 것), 아래 자세히 (줄 diff 또는 전 · 후) +- [x] 고른 버전 vs 바로 전 버전 비교는 worker +- [x] `[s]` 지금 파일을 버전으로 저장 +- [x] tests: 저장 1개 → `[s]` → 2개, 트리 제목에 "model 1 · prompt 1 · flow 5" + +## 설치 (R14) +- [x] `[project.scripts] tver = "tver.cli:main"`, 저장소 기본 `~/.tver/store` (`--repo` · `$TVER_REPO`) +- 확인: `uv tool install --editable .`, `cd /tmp && tver --help` + +## 엔진 3단계 · 키 거부 · JSON · 폴더 (R7 · R11 · R13) +- [x] `find_secrets`: `sk-…` · AWS · GitHub · Slack 토큰 모양, 메시지에는 앞 5자 + 끝 2자만 +- [x] `--json`, 종료 코드 0 / 1 / 2 +- [x] `save <폴더>`: `nodes` · `connections`가 있는 것만, 나머지 skipped + +## 엔진 4단계 · n8n에서 가져오기 · 꺼내기 · 이름 바꿈 (R9 · R10 · R12) +- [x] `pull`: `docker exec <컨테이너> n8n export:workflow --all` → 이름별 저장 (n8n에는 쓰지 않음) +- [x] `restore -o <파일>`: `git show`로 꺼내 파일로, 저장소는 그대로 +- [x] 짝 없는 − · + 노드 중 종류 · 설정이 같은 한 쌍 → `renamed`, 연결도 새 이름으로 맞춰 비교 diff --git a/Last3Mon/74/tver/README.md b/Last3Mon/74/tver/README.md new file mode 100644 index 0000000..a6dd353 --- /dev/null +++ b/Last3Mon/74/tver/README.md @@ -0,0 +1,48 @@ +# tver · 강사가 수업 준비 때 직접 만든 본 (2026-10-27) + +> **정답 자료** · 학생 저장소에는 수업 뒤 [VIBE-A-TVER.md](../VIBE-A-TVER.md)와 함께 올립니다. +> 이 본은 강사가 CLI 에이전트에 단계 요청문을 보내 만든 것이 아니라, Claude Code가 VIBE-A의 SPEC · IMP-PLAN을 보고 직접 썼습니다. 학생이 만든 것과 파일 구성이 달라도 됩니다. 요구 R1~R14의 **확인 방법**이 같으면 됩니다. + +## 설치 · 실행 + +```bash +cd ~/aio-01d4-stu/Last3Mon/74/tver +uv sync && uv run pytest -q # 테스트 11개 (저장소는 tmp_path, 가짜 키는 테스트 안에서 만듦) +uv tool install --editable . # 어느 폴더에서나 tver (R14) +cd /tmp && tver --help +``` + +| 하고 싶은 것 | 명령 | +| --- | --- | +| 두 파일 비교 | `tver diff a.json b.json` · 줄 diff 없이 `--short` · 다른 프로그램용 `--json` | +| 버전으로 저장 | `tver save my-work/helpdesk.json` · 폴더 `tver save my-work` (워크플로우만) | +| 버전 목록 · 비교 | `tver log helpdesk.json` · `tver diff helpdesk.json` | +| 한 버전 보기 · 꺼내기 | `tver show helpdesk.json` · `tver restore helpdesk.json -o my-work/helpdesk.json` | +| n8n에서 모두 가져오기 | `tver pull [--container day65-n8n-n8n-1]` (읽기만) | +| 화면(TUI) | `tver my-work/helpdesk.json` (`[s]` 지금 파일을 저장, `[q]` 끝) | + +- 저장소: `~/.tver/store` (또는 `--repo <폴더>`, `$TVER_REPO`). 내 프로젝트 폴더 밖에 따로 둡니다. +- 종료 코드: `diff` 0 같음 · 1 다름, 모든 명령 2 오류 (파일이 없음 · 워크플로우가 아님 · 키가 있어 저장 거부). + +## 파일 + +| 파일 | 하는 일 | 요구 | +| --- | --- | --- | +| `src/tver/workflow.py` | 읽기, 뜻 없는 칸 빼기(`clean`), 키 정렬 저장 글(`dumps`), 키 모양 찾기(가려서) | R2 · R7 | +| `src/tver/diff.py` | 노드 이름으로 짝짓기, 설정 · 연결 비교, 줄 diff, 모델 · 지시문 · 흐름 분류, 이름 바꿈 | R1 · R3 · R4 · R12 | +| `src/tver/store.py` | git 저장소: 저장(같으면 안 함) · 목록 · 꺼내기, 폴더 저장, n8n에서 내보내기 | R5 · R6 · R9 · R10 · R13 | +| `src/tver/cli.py` | 명령 · `--json` · 종료 코드 | R11 | +| `src/tver/app.py` | Textual 화면 | R8 | +| `tests/` | v1 · v2(`tests/data`, 비밀 없음)로 엔진 · 저장 · 화면 | | +| `Doc/IMP-PLAN.md` | 만든 순서 (엔진 1 → 2 → TUI 1 → 설치 → 엔진 3 · 4) | | + +## 2026-10-27에 확인한 것 + +| 확인 | 결과 | +| --- | --- | +| `uv run pytest -q` | 11 passed | +| `tver diff reference/helpdesk-v1.json reference/helpdesk-v2.json --short` | `flow.py diff`와 같은 7줄, `7 change(s) · model 1 · prompt 1 · flow 5`, 종료 코드 1 | +| 오늘 Lab의 helpdesk 정답 여섯 단계를 차례로 `save` | 버전 6개, 메시지 예 "15 change(s) · model 1 · prompt 1 · flow 13" (Lab 01 과제 2 → Lab 02 예제 1) | +| `tver save answers` | 워크플로우 5개 저장, 테스트 파일 `74-lab02-challenge-golden.json`은 skipped | +| `tver pull` (강사 n8n) | 192 new version(s) — 강사 n8n에 있던 워크플로우 전부, n8n은 그대로 | +| `uv tool install --editable .` → `/tmp`에서 `tver restore` → 원본과 `tver diff` | `0 change(s)`, 종료 코드 0 | diff --git a/Last3Mon/74/tver/pyproject.toml b/Last3Mon/74/tver/pyproject.toml new file mode 100644 index 0000000..332eb6a --- /dev/null +++ b/Last3Mon/74/tver/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "tver" +version = "0.1.0" +description = "Keep n8n workflow files as versions in git and compare two versions node by node" +requires-python = ">=3.11" +dependencies = ["textual>=0.80"] + +[project.scripts] +tver = "tver.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/tver"] + +[dependency-groups] +dev = ["pytest>=8", "pytest-asyncio>=0.23"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/Last3Mon/74/tver/src/tver/__init__.py b/Last3Mon/74/tver/src/tver/__init__.py new file mode 100644 index 0000000..acc35f6 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/__init__.py @@ -0,0 +1 @@ +"""tver: n8n workflow versions in git, compared node by node.""" diff --git a/Last3Mon/74/tver/src/tver/app.py b/Last3Mon/74/tver/src/tver/app.py new file mode 100644 index 0000000..4770bf1 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/app.py @@ -0,0 +1,101 @@ +"""Textual screen (R8): versions on the left, changed nodes as a tree on the right, the chosen change below. +`[s]` saves the file again as a new version, `[q]` quits.""" +from pathlib import Path + +from textual import work +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Footer, Header, ListItem, ListView, Static, Tree + +from . import diff +from .store import Store, safe_name +from .workflow import load + + +class VersionApp(App): + TITLE = "tver" + BINDINGS = [("s", "save", "Save file as a version"), ("q", "quit", "Quit")] + CSS = """ + #summary { height: auto; margin: 0 1; } + #versions { width: 40; border: round $accent; } + #changes { border: round $accent; } + #detail { height: 10; border: round $accent; padding: 0 1; } + """ + + def __init__(self, target, store: Store | None = None): + super().__init__() + self.file = Path(target) if Path(target).exists() else None + self.name_ = safe_name(Path(target).name) + self.store = store or Store() + self.versions = [] + self.changes = [] + + def compose(self) -> ComposeResult: + yield Header() + yield Static("loading...", id="summary") + with Horizontal(): + yield ListView(id="versions") + yield Tree("changes", id="changes") + yield Static("", id="detail") + yield Footer() + + def on_mount(self): + self.sub_title = f"{self.name_} · {self.store.repo}" + self.reload() + + def reload(self): + self.versions = self.store.log(self.name_) + view = self.query_one(ListView) + view.clear() + for v in self.versions: + view.append(ListItem(Static(f"{v.rev} {v.date[5:16]} {v.message.split(': ', 1)[-1]}"))) + self.query_one("#summary", Static).update( + f"{len(self.versions)} version(s)" + ("" if self.versions else f" · none yet: press [s] or run tver save {self.file or self.name_}")) + if self.versions: + view.index = 0 + self.compare(0) + + def on_list_view_highlighted(self, event): + if event.list_view.index is not None and 0 <= event.list_view.index < len(self.versions): + self.compare(event.list_view.index) + + @work(thread=True, exclusive=True) + def compare(self, index: int): + """The chosen version against the one before it (the oldest against nothing).""" + new = self.store.show(self.versions[index].rev, self.name_) + old = self.store.show(self.versions[index + 1].rev, self.name_) if index + 1 < len(self.versions) \ + else {"nodes": [], "connections": {}} + self.call_from_thread(self.show_changes, index, diff.compare(old, new)) + + def show_changes(self, index: int, changes): + self.changes = changes + tree = self.query_one(Tree) + tree.clear() + before = self.versions[index + 1].rev if index + 1 < len(self.versions) else "(empty)" + tree.root.set_label(f"{before} → {self.versions[index].rev} · {diff.summary_line(diff.summary(changes))}") + groups = {} + for i, change in enumerate(changes): + parent = groups.get(change.node) + if parent is None: + parent = groups[change.node] = tree.root.add(change.node, expand=True) + parent.add_leaf(f"[{change.category}] {change.title()}", data=i) + tree.root.expand() + self.query_one("#detail", Static).update("choose a change" if changes else "no change") + + def on_tree_node_highlighted(self, event): + index = event.node.data + if index is None or not 0 <= index < len(self.changes): + return + change = self.changes[index] + rows = [change.title()] + rows += change.lines or [f"- {diff.short(change.before, 200)}", f"+ {diff.short(change.after, 200)}"] \ + if change.op == "~set" else [] + self.query_one("#detail", Static).update("\n".join(rows[:12])) + + def action_save(self): + if not self.file: + self.query_one("#summary", Static).update("open tver with a file path to save it") + return + status, message = self.store.save_workflow(load(self.file), self.name_) + self.reload() + self.query_one("#summary", Static).update(f"{status} {message}".strip() + f" · {len(self.versions)} version(s)") diff --git a/Last3Mon/74/tver/src/tver/cli.py b/Last3Mon/74/tver/src/tver/cli.py new file mode 100644 index 0000000..6044964 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/cli.py @@ -0,0 +1,121 @@ +"""Command line. Exit codes: diff 0 same · 1 different, 2 error (any command).""" +import argparse +import json +import sys +from pathlib import Path + +from . import diff +from .store import DEFAULT_REPO, Store, StoreError, export_from_n8n, safe_name +from .workflow import NotWorkflow, load + +COMMANDS = ("diff", "save", "log", "show", "restore", "pull") + + +def load_side(store: Store, spec: str, name: str | None) -> dict: + """A file path, or a revision of in the store.""" + if Path(spec).exists(): + return load(spec) + if not name: + raise StoreError(f"{spec}: no such file (to compare versions give the workflow name: tver diff )") + return store.show(spec, name) + + +def print_changes(changes, full=False): + for change in changes: + print(change.title()) + if change.lines and full: + for row in change.lines: + print(f" {row}") + print(diff.summary_line(diff.summary(changes))) + + +def run_diff(args, store): + old = load_side(store, args.old, args.name) + new = load_side(store, args.new, args.name) + changes = diff.compare(old, new) + if args.json: + print(json.dumps({"changes": [c.to_dict() for c in changes], "summary": diff.summary(changes)}, + ensure_ascii=False, indent=2)) + else: + print(f"{old.get('name', args.old)} → {new.get('name', args.new)}") + print_changes(changes, full=not args.short) + return 1 if changes else 0 + + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + if argv and argv[0] not in COMMANDS and not argv[0].startswith("-"): + from .app import VersionApp # `tver ` opens the TUI + repo = None + if "--repo" in argv: + repo = argv[argv.index("--repo") + 1] + VersionApp(argv[0], Store(repo)).run() + sys.exit(0) + p = argparse.ArgumentParser(prog="tver", description="n8n workflow versions in git, compared node by node. " + "`tver ` opens the TUI.") + p.add_argument("--repo", default=None, help=f"version store (default {DEFAULT_REPO}, or $TVER_REPO)") + sub = p.add_subparsers(dest="cmd", required=True) + d = sub.add_parser("diff", help="compare two files, or two versions: tver diff ") + d.add_argument("old") + d.add_argument("new") + d.add_argument("name", nargs="?", help="workflow name in the store when old/new are revisions") + d.add_argument("--plain", action="store_true", help="plain lines (default)") + d.add_argument("--short", action="store_true", help="no line diff for long text") + d.add_argument("--json", action="store_true", help="JSON for other programs") + s = sub.add_parser("save", help="save a workflow file (or every workflow file in a folder) as a new version") + s.add_argument("path") + s.add_argument("--allow-secrets", action="store_true", help="save even if it looks like it has a secret (not advised)") + lg = sub.add_parser("log", help="versions of one workflow") + lg.add_argument("name") + lg.add_argument("--json", action="store_true") + sh = sub.add_parser("show", help="print one version") + sh.add_argument("rev") + sh.add_argument("name") + r = sub.add_parser("restore", help="write an old version to a file (then push it yourself)") + r.add_argument("rev") + r.add_argument("name") + r.add_argument("-o", "--out", required=True, help="file to write, e.g. my-work/helpdesk.json") + pl = sub.add_parser("pull", help="export every workflow from the running n8n and save each as a version") + pl.add_argument("--container", default="day65-n8n-n8n-1") + args = p.parse_args(argv) + store = Store(args.repo) + try: + if args.cmd == "diff": + return run_diff(args, store) + if args.cmd == "save": + rows = store.save(args.path, args.allow_secrets) + for name, status, message in rows: + print(f"{status:9} {name}" + (f" {message}" if message and status == "saved" else "")) + return 0 + if args.cmd == "log": + versions = store.log(args.name) + if args.json: + print(json.dumps([v.__dict__ for v in versions], ensure_ascii=False, indent=2)) + for v in versions if not args.json else []: + print(f"{v.rev} {v.date} {v.message}") + if not versions: + print(f"no versions of {safe_name(Path(args.name).name)} in {store.repo}", file=sys.stderr) + return 0 + if args.cmd == "show": + print(json.dumps(store.show(args.rev, args.name), ensure_ascii=False, indent=2)) + return 0 + if args.cmd == "restore": + out = store.restore(args.rev, args.name, args.out) + print(f"wrote {out} from {args.rev}. Push it yourself: uv run tools/flow.py push {out}") + return 0 + if args.cmd == "pull": + saved = 0 + for workflow in export_from_n8n(args.container): + status, message = store.save_workflow(workflow, safe_name(str(workflow.get("name", "workflow")))) + saved += status == "saved" + print(f"{status:9} {safe_name(str(workflow.get('name')))}" + (f" {message}" if status == "saved" else "")) + print(f"{saved} new version(s) in {store.repo}") + return 0 + except (StoreError, NotWorkflow) as error: + print(f"tver: {error}", file=sys.stderr) + return 2 + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Last3Mon/74/tver/src/tver/diff.py b/Last3Mon/74/tver/src/tver/diff.py new file mode 100644 index 0000000..01164e6 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/diff.py @@ -0,0 +1,147 @@ +"""Node-by-node difference of two workflows (R1), line diff for long text (R3), +model · prompt · flow summary (R4), renamed nodes (R12).""" +import difflib +import json +from dataclasses import asdict, dataclass, field + +from .workflow import NOISE + +LONG = 80 +PROMPT_KEYS = ("systemMessage", "text", "jsCode", "pythonCode", "prompt") + + +@dataclass +class Change: + op: str # "+node" "-node" "~set" "renamed" "+link" "-link" + node: str + category: str # "model" | "prompt" | "flow" + key: str = "" + before: object = None + after: object = None + lines: list = field(default_factory=list) # unified diff lines for long text + + def to_dict(self): + return asdict(self) + + def title(self): + if self.op == "+node": + return f"+ node '{self.node}' ({self.after})" + if self.op == "-node": + return f"- node '{self.node}' ({self.before})" + if self.op == "renamed": + return f"~ renamed '{self.before}' → '{self.after}'" + if self.op in ("+link", "-link"): + return f"{self.op[0]} link {self.node} → {self.after if self.op == '+link' else self.before} ({self.key})" + if self.lines: + return f"~ '{self.node}' {self.key}: changed ({len(text(self.before))} → {len(text(self.after))} chars)" + return f"~ '{self.node}' {self.key}: {short(self.before)} → {short(self.after)}" + + +def text(value) -> str: + if value is None: + return "(none)" + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + + +def short(value, width=60) -> str: + s = text(value) + return s if len(s) <= width else s[: width - 1] + "…" + + +def flat(value, prefix=""): + if isinstance(value, dict) and value: + out = {} + for key, item in value.items(): + out.update(flat(item, f"{prefix}.{key}" if prefix else str(key))) + return out + return {prefix: value} + + +def kind(node) -> str: + return str(node.get("type", "?")).split(".")[-1] + + +def category(node, key: str) -> str: + """Chat model's model → model; Agent / chain text · systemMessage and Code → prompt; the rest → flow.""" + t = kind(node).lower() + last = key.split(".")[-1] + if t.startswith("lmchat") or t.startswith("lm") or "embeddings" in t: + if last in ("model", "value", "modelName") and ("model" in key): + return "model" + if last in PROMPT_KEYS: + return "prompt" + return "flow" + + +def node_settings(node) -> dict: + body = {k: v for k, v in node.items() if k not in NOISE and k != "name"} + return flat(body) + + +def links(workflow) -> set: + out = set() + for src, kinds in (workflow.get("connections") or {}).items(): + for link_kind, outputs in (kinds or {}).items(): + for index, output in enumerate(outputs or []): + for dst in output or []: + out.add((src, f"{link_kind}[{index}]", dst.get("node"))) + return out + + +def line_diff(before, after) -> list[str]: + a, b = text(before).splitlines(), text(after).splitlines() + return [row for row in difflib.unified_diff(a, b, "before", "after", lineterm="", n=1)][2:] + + +def compare(old: dict, new: dict) -> list[Change]: + old_nodes = {n.get("name"): n for n in old.get("nodes", [])} + new_nodes = {n.get("name"): n for n in new.get("nodes", [])} + gone = sorted(set(old_nodes) - set(new_nodes)) + added = sorted(set(new_nodes) - set(old_nodes)) + renamed = {} + for name in list(gone): + twin = [a for a in added if kind(new_nodes[a]) == kind(old_nodes[name]) + and node_settings(new_nodes[a]) == node_settings(old_nodes[name])] + if len(twin) == 1: + renamed[name] = twin[0] + gone.remove(name) + added.remove(twin[0]) + changes = [] + for name in added: + changes.append(Change("+node", name, "flow", after=kind(new_nodes[name]))) + for name in gone: + changes.append(Change("-node", name, "flow", before=kind(old_nodes[name]))) + for old_name, new_name in sorted(renamed.items()): + changes.append(Change("renamed", new_name, "flow", before=old_name, after=new_name)) + pairs = [(n, n) for n in sorted(set(old_nodes) & set(new_nodes))] + sorted(renamed.items()) + for old_name, new_name in pairs: + a, b = node_settings(old_nodes[old_name]), node_settings(new_nodes[new_name]) + for key in sorted(set(a) | set(b)): + if a.get(key) == b.get(key): + continue + before, after = a.get(key), b.get(key) + long = max(len(text(before)), len(text(after))) > LONG or "\n" in text(before) + text(after) + changes.append(Change("~set", new_name, category(new_nodes[new_name], key), key, before, after, + line_diff(before, after) if long else [])) + rename = {o: n for o, n in renamed.items()} + old_links = {(rename.get(s, s), k, rename.get(d, d)) for s, k, d in links(old)} + new_links = links(new) + for src, link_kind, dst in sorted(new_links - old_links): + changes.append(Change("+link", src, "flow", link_kind, after=dst)) + for src, link_kind, dst in sorted(old_links - new_links): + changes.append(Change("-link", src, "flow", link_kind, before=dst)) + return changes + + +def summary(changes: list[Change]) -> dict: + counts = {"model": 0, "prompt": 0, "flow": 0} + for change in changes: + counts[change.category] += 1 + nodes = {"+": sum(c.op == "+node" for c in changes), "-": sum(c.op == "-node" for c in changes), + "~": len({c.node for c in changes if c.op == "~set"}), "renamed": sum(c.op == "renamed" for c in changes)} + return {"total": len(changes), **counts, "nodes": nodes, + "links": {"+": sum(c.op == "+link" for c in changes), "-": sum(c.op == "-link" for c in changes)}} + + +def summary_line(s: dict) -> str: + return f"{s['total']} change(s) · model {s['model']} · prompt {s['prompt']} · flow {s['flow']}" diff --git a/Last3Mon/74/tver/src/tver/store.py b/Last3Mon/74/tver/src/tver/store.py new file mode 100644 index 0000000..f0bc9c0 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/store.py @@ -0,0 +1,118 @@ +"""Versions in a git repository (R5 save · R6 log/show · R10 restore · R13 folder · R9 pull from n8n).""" +import json +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from . import diff +from .workflow import NotWorkflow, dumps, find_secrets, is_workflow, load, parse + +DEFAULT_REPO = Path(os.getenv("TVER_REPO", Path.home() / ".tver" / "store")) + + +class StoreError(RuntimeError): + pass + + +@dataclass +class Version: + rev: str + date: str + message: str + + +def safe_name(name: str) -> str: + keep = "".join(ch if ch.isalnum() or ch in "-_." else "-" for ch in name.strip()) + return (keep or "workflow").lower() + ("" if keep.endswith(".json") else ".json") + + +class Store: + def __init__(self, repo=None): + self.repo = Path(repo or DEFAULT_REPO).expanduser() + + def git(self, *args, check=True) -> str: + result = subprocess.run(["git", "-C", str(self.repo), *args], capture_output=True, text=True) + if check and result.returncode != 0: + raise StoreError((result.stderr or result.stdout).strip() or f"git {args[0]} failed") + return result.stdout + + def ensure(self): + if not (self.repo / ".git").exists(): + self.repo.mkdir(parents=True, exist_ok=True) + self.git("init", "-q") + self.git("config", "user.name", "tver") + self.git("config", "user.email", "tver@localhost") + + def save_workflow(self, workflow: dict, name: str, force_secrets=False) -> tuple[str, str]: + """Write the cleaned JSON as and commit when it changed. Returns (status, message).""" + text = dumps(workflow) + secrets = find_secrets(text) + if secrets and not force_secrets: + raise StoreError(f"{name}: looks like a secret ({', '.join(secrets)}). Not saved. " + "Remove it (use an n8n Credential) and save again.") + self.ensure() + target = self.repo / name + previous = target.read_text(encoding="utf-8") if target.exists() else None + if previous == text: + return "no change", "" + message = f"{name}: new" if previous is None else \ + f"{name}: " + diff.summary_line(diff.summary(diff.compare(parse(previous, name), parse(text, name)))) + target.write_text(text, encoding="utf-8") + self.git("add", name) + self.git("commit", "-q", "-m", message) + return "saved", message + + def save(self, path, force_secrets=False) -> list[tuple[str, str, str]]: + """A file, or every workflow file in a folder (others are skipped). Returns (name, status, message).""" + path = Path(path) + files = sorted(path.glob("*.json")) if path.is_dir() else [path] + out = [] + for file in files: + try: + workflow = load(file) + except NotWorkflow: + if path.is_dir(): + out.append((file.name, "skipped", "not a workflow")) + continue + raise + status, message = self.save_workflow(workflow, safe_name(file.name), force_secrets) + out.append((safe_name(file.name), status, message)) + return out + + def names(self) -> list[str]: + if not (self.repo / ".git").exists(): + return [] + return sorted(line for line in self.git("ls-files").splitlines() if line.endswith(".json")) + + def log(self, name: str) -> list[Version]: + name = safe_name(Path(name).name) + if not (self.repo / ".git").exists(): + return [] + rows = self.git("log", "--format=%h%x09%ad%x09%s", "--date=format:%Y-%m-%d %H:%M:%S", "--", name, check=False) + return [Version(*row.split("\t", 2)) for row in rows.splitlines() if row.count("\t") >= 2] + + def show(self, rev: str, name: str) -> dict: + name = safe_name(Path(name).name) + return parse(self.git("show", f"{rev}:{name}"), f"{rev}:{name}") + + def restore(self, rev: str, name: str, out) -> Path: + """Write that version to a file (the person pushes it to n8n). The store itself is not changed.""" + workflow = self.show(rev, name) + out = Path(out) + out.write_text(json.dumps(workflow, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return out + + +def export_from_n8n(container: str) -> list[dict]: + """R9: every workflow in the running n8n container (docker exec ... n8n export:workflow --all).""" + command = ["docker", "exec", container, "sh", "-c", + "n8n export:workflow --all --output=/tmp/tver-all.json >/dev/null 2>&1; cat /tmp/tver-all.json; rm -f /tmp/tver-all.json"] + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=120) + except (OSError, subprocess.TimeoutExpired) as error: + raise StoreError(f"docker exec failed: {error}") from error + if result.returncode != 0 or not result.stdout.strip(): + raise StoreError((result.stderr or "no output").strip() + f"\n Is the n8n container '{container}' running?") + data = json.loads(result.stdout) + return [item for item in (data if isinstance(data, list) else [data]) if is_workflow(item)] diff --git a/Last3Mon/74/tver/src/tver/workflow.py b/Last3Mon/74/tver/src/tver/workflow.py new file mode 100644 index 0000000..fcf4010 --- /dev/null +++ b/Last3Mon/74/tver/src/tver/workflow.py @@ -0,0 +1,55 @@ +"""Read and clean n8n workflow JSON: drop values that change without meaning (R2), find secrets (R7).""" +import json +import re +from pathlib import Path + +NOISE = {"id", "position", "webhookId", "versionId", "meta", "pinData", "updatedAt", "createdAt", "active", "shared", "tags"} +SECRET = re.compile(r"\b(sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|xox[abp]-[A-Za-z0-9-]{10,})") + + +class NotWorkflow(ValueError): + pass + + +def is_workflow(data) -> bool: + return isinstance(data, dict) and isinstance(data.get("nodes"), list) and isinstance(data.get("connections"), dict) + + +def load(path) -> dict: + try: + data = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise NotWorkflow(f"{path}: {error}") from error + if not is_workflow(data): + raise NotWorkflow(f"{path}: not an n8n workflow (needs nodes and connections)") + return data + + +def parse(text: str, label: str = "workflow") -> dict: + try: + data = json.loads(text) + except json.JSONDecodeError as error: + raise NotWorkflow(f"{label}: {error}") from error + if not is_workflow(data): + raise NotWorkflow(f"{label}: not an n8n workflow") + return data + + +def clean(workflow: dict) -> dict: + """Same meaning → same JSON: no ids, positions or export stamps; nodes sorted by name.""" + nodes = [] + for node in workflow.get("nodes", []): + nodes.append({k: v for k, v in node.items() if k not in NOISE}) + nodes.sort(key=lambda n: str(n.get("name", ""))) + out = {k: v for k, v in workflow.items() if k not in NOISE | {"nodes"}} + out["nodes"] = nodes + return out + + +def dumps(workflow: dict) -> str: + return json.dumps(clean(workflow), ensure_ascii=False, indent=2, sort_keys=True) + "\n" + + +def find_secrets(text: str) -> list[str]: + """Masked secret-looking values (sk-… keys etc.). Credential references (id · name) are fine.""" + return [m.group(0)[:5] + "…" + m.group(0)[-2:] for m in SECRET.finditer(text)] diff --git a/Last3Mon/74/tver/tests/conftest.py b/Last3Mon/74/tver/tests/conftest.py new file mode 100644 index 0000000..7e06101 --- /dev/null +++ b/Last3Mon/74/tver/tests/conftest.py @@ -0,0 +1,33 @@ +import json +import shutil +from pathlib import Path + +import pytest + +DATA = Path(__file__).parent / "data" + + +@pytest.fixture +def v1(): + return json.loads((DATA / "helpdesk-v1.json").read_text(encoding="utf-8")) + + +@pytest.fixture +def v2(): + return json.loads((DATA / "helpdesk-v2.json").read_text(encoding="utf-8")) + + +@pytest.fixture +def work(tmp_path): + """A my-work folder with v1 as helpdesk.json, a note and a cases file (not workflows).""" + folder = tmp_path / "my-work" + folder.mkdir() + shutil.copy(DATA / "helpdesk-v1.json", folder / "helpdesk.json") + (folder / "PROMPTS.md").write_text("# prompts\n", encoding="utf-8") + (folder / "cases.json").write_text('{"webhook": "x", "cases": []}', encoding="utf-8") + return folder + + +@pytest.fixture +def store_dir(tmp_path): + return tmp_path / "store" diff --git a/Last3Mon/74/tver/tests/data/helpdesk-v1.json b/Last3Mon/74/tver/tests/data/helpdesk-v1.json new file mode 100644 index 0000000..3b59da5 --- /dev/null +++ b/Last3Mon/74/tver/tests/data/helpdesk-v1.json @@ -0,0 +1,286 @@ +{ + "name": "74 IT helpdesk v1 (provided)", + "nodes": [ + { + "name": "Question webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "rel-v1", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "0709e90d-e8ea-509c-8c91-1721c012d260", + "position": [ + 0, + 300 + ], + "id": "d087d97a-46d1-544f-b742-78984643db81" + }, + { + "name": "Has question?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.question }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "0cafcebc-1ba4-546e-aa2e-22f24643ca5a" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"question is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "f60c3d44-3793-50e4-89c1-96c15c0075b0" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": {}, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "position": [ + 440, + 300 + ], + "id": "7bf6486f-83e4-56c6-9052-ee94305aa025" + }, + { + "name": "Get ticket", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "position": [ + 660, + 300 + ], + "id": "9f1e31b4-c9b1-5848-8bc7-4f9f5c5b905b" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}", + "options": { + "systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.", + "maxIterations": 1 + } + }, + "position": [ + 880, + 300 + ], + "id": "28d25947-3225-58fb-adec-112be9f61a60" + }, + { + "name": "Answer model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "gpt-4.1-mini" + }, + "options": { + "temperature": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 880, + 520 + ], + "id": "008c5ad6-673c-5e09-ae15-bbcf04777148" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nreturn [{ json: result }];" + }, + "position": [ + 1100, + 300 + ], + "id": "5eabccbd-9c75-59f6-ac72-9070ef5f3a89" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1320, + 300 + ], + "id": "95e15847-a230-567c-a1f5-827a415a52a4" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/tver/tests/data/helpdesk-v2.json b/Last3Mon/74/tver/tests/data/helpdesk-v2.json new file mode 100644 index 0000000..1693992 --- /dev/null +++ b/Last3Mon/74/tver/tests/data/helpdesk-v2.json @@ -0,0 +1,310 @@ +{ + "name": "74 IT helpdesk v2 (provided)", + "nodes": [ + { + "name": "Question webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "rel-v2", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "4dd9ca0d-6e35-58d9-afb9-36689beca2c7", + "position": [ + 0, + 300 + ], + "id": "edf3da10-d647-5671-a906-292126b204df" + }, + { + "name": "Has question?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.body.question }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 220, + 300 + ], + "id": "cf8a9038-ff3e-56a3-a17f-9c062bdd0391" + }, + { + "name": "Reject empty", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "{ \"error\": \"question is required\" }", + "options": { + "responseCode": 400 + } + }, + "position": [ + 440, + 560 + ], + "id": "3e81f84e-0040-52a4-80a2-e093464dea75" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": {}, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "position": [ + 440, + 300 + ], + "id": "7e858b12-8c3b-5b72-aa02-09276f0d63e0" + }, + { + "name": "Get ticket", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "position": [ + 660, + 300 + ], + "id": "9265daab-0313-5c12-aa77-7a596f23eae5" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}", + "options": { + "systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.\nWhen the answer comes from a FAQ entry, end with its id in brackets, for example [FAQ-003]. Keep answers short.", + "maxIterations": 1 + } + }, + "position": [ + 880, + 300 + ], + "id": "027a9c55-9b22-5122-85e5-9f8e347fe189" + }, + { + "name": "Answer model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "gpt-5.4-nano" + }, + "options": { + "temperature": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 880, + 520 + ], + "id": "bfe53f6e-ecec-5876-8943-03d0b6dad9a5" + }, + { + "name": "Review check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "let answer = String($json.output ?? '').trim();\nif (answer.length > 400) answer = answer.slice(0, 400);\nreturn [{ json: { output: answer } }];" + }, + "position": [ + 1100, + 300 + ], + "id": "0d0c0486-e241-521a-83ca-66b725fd05d0" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nreturn [{ json: result }];" + }, + "position": [ + 1320, + 300 + ], + "id": "27c64881-2145-56c1-ab88-8f0a7515dbd2" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1540, + 300 + ], + "id": "5a7d65a6-3b4e-562d-bf7e-1be3756a8cde" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "Review check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review check": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/74/tver/tests/test_app.py b/Last3Mon/74/tver/tests/test_app.py new file mode 100644 index 0000000..e88a678 --- /dev/null +++ b/Last3Mon/74/tver/tests/test_app.py @@ -0,0 +1,34 @@ +"""R8: versions list, change tree, detail of the chosen change, [s] saves.""" +import asyncio +import json + +from textual.widgets import ListView, Tree + +from tver.app import VersionApp +from tver.store import Store + + +async def wait_for(pilot, check): + for _ in range(60): + await asyncio.sleep(0.05) + await pilot.pause() + if check(): + return True + return False + + +async def test_versions_tree_and_save(work, store_dir, v2): + store = Store(store_dir) + store.save(work / "helpdesk.json") + (work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8") + app = VersionApp(str(work / "helpdesk.json"), store) + async with app.run_test() as pilot: + assert len(app.query_one(ListView).children) == 1 + await pilot.press("s") + assert await wait_for(pilot, lambda: len(app.query_one(ListView).children) == 2) + assert await wait_for(pilot, lambda: "model 1 · prompt 1 · flow 5" in str(app.query_one(Tree).root.label)) + tree = app.query_one(Tree) + tree.focus() + await pilot.press("down", "down") + await pilot.pause() + assert str(app.query_one("#detail").render()).strip() != "" diff --git a/Last3Mon/74/tver/tests/test_diff.py b/Last3Mon/74/tver/tests/test_diff.py new file mode 100644 index 0000000..44cec12 --- /dev/null +++ b/Last3Mon/74/tver/tests/test_diff.py @@ -0,0 +1,60 @@ +"""R1 · R2 · R3 · R4 · R11 · R12 with the day 74 v1 → v2 files.""" +import copy +import json + +from tver import diff +from tver.cli import main +from tver.workflow import clean, dumps + + +def test_v1_to_v2_is_seven_changes_like_flow_py(v1, v2): + changes = diff.compare(v1, v2) + titles = [c.title() for c in changes] + assert "+ node 'Review check' (code)" in titles + assert any(t.startswith("~ 'Answer' parameters.options.systemMessage: changed") for t in titles) + assert "~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano" in titles + assert "~ 'Question webhook' parameters.path: rel-v1 → rel-v2" in titles + s = diff.summary(changes) + assert (s["total"], s["model"], s["prompt"], s["flow"]) == (7, 1, 1, 5) + assert s["links"] == {"+": 2, "-": 1} + + +def test_noise_is_not_a_change(v1): + moved = copy.deepcopy(v1) + for node in moved["nodes"]: + node["position"] = [node["position"][0] + 100, 0] + node["id"] = "other-" + node["name"] + moved["nodes"].reverse() + assert diff.compare(v1, moved) == [] + assert dumps(v1) == dumps(moved) + assert all("position" not in n and "id" not in n for n in clean(v1)["nodes"]) + + +def test_long_prompt_gets_line_diff(v1, v2): + change = next(c for c in diff.compare(v1, v2) if c.key.endswith("systemMessage")) + assert change.category == "prompt" + assert any(row.startswith("+") and "[FAQ-003]" in row for row in change.lines) + + +def test_renamed_node_is_paired(v1): + renamed = copy.deepcopy(v1) + for node in renamed["nodes"]: + if node["name"] == "Search FAQ": + node["name"] = "Find FAQ" + conn = json.dumps(renamed["connections"]).replace('"Search FAQ"', '"Find FAQ"') + renamed["connections"] = json.loads(conn) + changes = diff.compare(v1, renamed) + assert [c.op for c in changes] == ["renamed"] + assert (changes[0].before, changes[0].after) == ("Search FAQ", "Find FAQ") + + +def test_cli_diff_exit_codes_and_json(tmp_path, v1, v2, capsys): + a, b = tmp_path / "a.json", tmp_path / "b.json" + a.write_text(json.dumps(v1), encoding="utf-8") + b.write_text(json.dumps(v2), encoding="utf-8") + assert main(["diff", str(a), str(a)]) == 0 + capsys.readouterr() + assert main(["diff", str(a), str(b), "--json"]) == 1 + out = json.loads(capsys.readouterr().out) + assert out["summary"]["total"] == 7 + assert main(["diff", str(a), str(tmp_path / "missing.json")]) == 2 diff --git a/Last3Mon/74/tver/tests/test_store.py b/Last3Mon/74/tver/tests/test_store.py new file mode 100644 index 0000000..8f0734b --- /dev/null +++ b/Last3Mon/74/tver/tests/test_store.py @@ -0,0 +1,57 @@ +"""R5 save · R6 log/diff of versions · R7 no secrets · R10 restore · R13 folder.""" +import json + +import pytest + +from tver.cli import main +from tver.store import Store, StoreError + +FAKE_KEY = "sk-" + "test-" + "a" * 24 # made here, never stored in a file of this repository + + +def test_save_twice_makes_one_commit(work, store_dir): + store = Store(store_dir) + assert store.save(work / "helpdesk.json")[0][1] == "saved" + assert store.save(work / "helpdesk.json")[0][1] == "no change" + assert len(store.log("helpdesk.json")) == 1 + + +def test_versions_and_diff_between_them(work, store_dir, v2, capsys): + store = Store(store_dir) + store.save(work / "helpdesk.json") + (work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8") + name, status, message = store.save(work / "helpdesk.json")[0] + assert status == "saved" and "model 1 · prompt 1 · flow 5" in message + versions = store.log("helpdesk.json") + assert [v.message.split(": ")[0] for v in versions] == ["helpdesk.json", "helpdesk.json"] + assert main(["--repo", str(store_dir), "diff", versions[1].rev, versions[0].rev, "helpdesk.json", "--short"]) == 1 + assert "7 change(s) · model 1 · prompt 1 · flow 5" in capsys.readouterr().out + + +def test_secret_is_not_saved(work, store_dir): + data = json.loads((work / "helpdesk.json").read_text(encoding="utf-8")) + data["nodes"][0]["parameters"]["note"] = FAKE_KEY + (work / "helpdesk.json").write_text(json.dumps(data), encoding="utf-8") + with pytest.raises(StoreError) as error: + Store(store_dir).save(work / "helpdesk.json") + assert FAKE_KEY not in str(error.value) + assert not (store_dir / "helpdesk.json").exists() + + +def test_restore_writes_a_file_and_keeps_the_store(work, store_dir, v2, tmp_path): + store = Store(store_dir) + store.save(work / "helpdesk.json") + (work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8") + store.save(work / "helpdesk.json") + oldest = store.log("helpdesk.json")[-1].rev + out = store.restore(oldest, "helpdesk.json", tmp_path / "back.json") + restored = json.loads(out.read_text(encoding="utf-8")) + model = next(n for n in restored["nodes"] if n["name"] == "Answer model") + assert model["parameters"]["model"]["value"] == "gpt-4.1-mini" + assert len(store.log("helpdesk.json")) == 2 + + +def test_folder_save_skips_files_that_are_not_workflows(work, store_dir): + rows = Store(store_dir).save(work) + assert ("cases.json", "skipped", "not a workflow") in rows + assert [r[0] for r in rows if r[1] == "saved"] == ["helpdesk.json"] diff --git a/Last3Mon/74/tver/uv.lock b/Last3Mon/74/tver/uv.lock new file mode 100644 index 0000000..800e02e --- /dev/null +++ b/Last3Mon/74/tver/uv.lock @@ -0,0 +1,195 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/98/7a1a5f31fd5c7ba93e963b168e244b8e3dd705b3d2a718e3c3307583bf57/linkify_it_py-2.2.0.tar.gz", hash = "sha256:907acd2d17ac1fbb9ddb62c8957ccbd6158cac602231a15c3b0cd1e215f03cee", size = 32939, upload-time = "2026-08-29T07:07:08.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl", hash = "sha256:3adc40eb5af300b2605fcfdb968c24e1d780a90f1f2221af7c15e5111e94d443", size = 21971, upload-time = "2026-08-29T07:07:07.164Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/43/8c4b5fe23f0e119cf4e1b16c2d16240ab9057cdbb138568c91d5a48abf81/platformdirs-4.13.6.tar.gz", hash = "sha256:211d176e24674b5895dedf9021c262bcf9fad008a70610bfb147d4a8d6d8fe1b", size = 62750, upload-time = "2026-10-27T08:19:11.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/24/b312a9466839b1f708eed42790b47e927dc7f774a528bb49b1cdf8b734b9/platformdirs-4.13.6-py3-none-any.whl", hash = "sha256:21fc198fb09f3e78cadcdfd8a6ae4fd65cadc64a3cba78b694321c2996114a3e", size = 32767, upload-time = "2026-10-27T08:19:10.405Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/9a/887535e477847f5159377ab35303923644840b13c0ca03c0a467f84d4680/pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b", size = 145237, upload-time = "2026-10-25T21:05:37.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/d7/cd9e2708afdbf059658adaf08bdb0792ca52489871051d6ce7d498b51277/pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc", size = 17061, upload-time = "2026-10-25T21:05:35.473Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tver" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "textual" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [{ name = "textual", specifier = ">=0.80" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8" }, + { name = "pytest-asyncio", specifier = ">=0.23" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]