From d88c5122d571b1e722269191bd0e515bb4eb41ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EB=AC=B8=EC=B0=BD=40MBA?= Date: Mon, 26 Oct 2026 20:15:51 +0900 Subject: [PATCH] =?UTF-8?q?72=EC=9D=BC=EC=B0=A8=20=EC=A0=95=EB=8B=B5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=EC=8B=A4=EC=8A=B5=20=EC=A0=95=EB=8B=B5?= =?UTF-8?q?=EB=B3=B8=20=C2=B7=20answers=20=C2=B7=20VIBE-A=20=C2=B7=20tbenc?= =?UTF-8?q?h)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- Last3Mon/72/LAB-01-A-MEASURE-BOTTLENECK.md | 117 + Last3Mon/72/LAB-02-A-OPTIMIZE-FLOW.md | 143 + Last3Mon/72/LAB-03-A-STABILITY.md | 164 + Last3Mon/72/VIBE-A-TBENCH.md | 197 + Last3Mon/72/answers/00-lab01-baseline.txt | 47 + Last3Mon/72/answers/01-lab01-baseline.json | 95 + Last3Mon/72/answers/01-lab01-baseline.txt | 58 + Last3Mon/72/answers/02-lab01.json | 137 + Last3Mon/72/answers/02-lab01.txt | 106 + Last3Mon/72/answers/03-lab01-call.txt | 33 + Last3Mon/72/answers/04-lab01-kinds.txt | 92 + .../72/answers/05-lab01-challenge-before.json | 166 + .../72/answers/05-lab01-challenge-before.txt | 128 + Last3Mon/72/answers/06-lab01-challenge.json | 165 + Last3Mon/72/answers/06-lab01-challenge.txt | 136 + .../72/answers/07-lab01-after-challenge.json | 141 + .../72/answers/07-lab01-after-challenge.txt | 112 + .../answers/08-lab01-challenge2-before.json | 180 + .../72/answers/08-lab01-challenge2-before.txt | 138 + Last3Mon/72/answers/09-lab01-challenge2.json | 179 + Last3Mon/72/answers/09-lab01-challenge2.txt | 150 + .../72/answers/10-lab01-challenge-after2.json | 177 + .../72/answers/10-lab01-challenge-after2.txt | 148 + Last3Mon/72/answers/11-lab01-after2.json | 150 + Last3Mon/72/answers/11-lab01-after2.txt | 121 + Last3Mon/72/answers/12-lab02-rules.json | 332 + Last3Mon/72/answers/12-lab02-rules.txt | 268 + Last3Mon/72/answers/13-lab02-rules-bench.txt | 10 + Last3Mon/72/answers/14-lab02-cache.json | 319 + Last3Mon/72/answers/14-lab02-cache.txt | 261 + Last3Mon/72/answers/15-lab01-cache.json | 134 + Last3Mon/72/answers/15-lab01-cache.txt | 103 + Last3Mon/72/answers/16-cache-stats.txt | 3 + Last3Mon/72/answers/17-cache-hit-bench.txt | 26 + .../72/answers/18-lab02-challenge-before.json | 177 + .../72/answers/18-lab02-challenge-before.txt | 135 + Last3Mon/72/answers/19-bench-before.txt | 10 + Last3Mon/72/answers/20-lab02-challenge.json | 491 ++ Last3Mon/72/answers/20-lab02-challenge.txt | 397 + .../72/answers/21-lab02-after-challenge.json | 319 + .../72/answers/21-lab02-after-challenge.txt | 261 + .../72/answers/22-lab01-after-challenge.json | 132 + .../72/answers/22-lab01-after-challenge.txt | 103 + Last3Mon/72/answers/23-bench-after.txt | 10 + .../answers/24-lab02-challenge2-before.json | 223 + .../72/answers/24-lab02-challenge2-before.txt | 176 + Last3Mon/72/answers/25-lab02-challenge2.json | 217 + Last3Mon/72/answers/25-lab02-challenge2.txt | 177 + .../72/answers/26-lab02-challenge-after2.json | 173 + .../72/answers/26-lab02-challenge-after2.txt | 139 + Last3Mon/72/answers/27-lab02-after2.json | 328 + Last3Mon/72/answers/27-lab02-after2.txt | 270 + Last3Mon/72/answers/28-lab03-fault-before.txt | 53 + Last3Mon/72/answers/29-lab03.json | 112 + Last3Mon/72/answers/29-lab03.txt | 92 + Last3Mon/72/answers/30-lab02-resilient.json | 319 + Last3Mon/72/answers/30-lab02-resilient.txt | 261 + Last3Mon/72/answers/31-llm-down-before.txt | 19 + .../72/answers/32-lab03-llm-down-before.json | 84 + .../72/answers/32-lab03-llm-down-before.txt | 47 + Last3Mon/72/answers/33-lab03-llm-down.json | 90 + Last3Mon/72/answers/33-lab03-llm-down.txt | 64 + .../72/answers/34-lab02-llm-fallback.json | 319 + Last3Mon/72/answers/34-lab02-llm-fallback.txt | 261 + .../72/answers/35-lab03-llm-fallback.json | 112 + Last3Mon/72/answers/35-lab03-llm-fallback.txt | 92 + .../72/answers/36-lab03-ticket-before.json | 119 + .../72/answers/36-lab03-ticket-before.txt | 90 + Last3Mon/72/answers/37-lab03-ticket.json | 133 + Last3Mon/72/answers/37-lab03-ticket.txt | 110 + .../72/answers/38-lab03-after-ticket.json | 112 + Last3Mon/72/answers/38-lab03-after-ticket.txt | 92 + .../39-lab03-llm-down-after-ticket.json | 90 + .../39-lab03-llm-down-after-ticket.txt | 64 + Last3Mon/72/answers/40-lab03-challenge.json | 94 + Last3Mon/72/answers/40-lab03-challenge.txt | 72 + .../answers/41-lab03-challenge2-before.json | 123 + .../72/answers/41-lab03-challenge2-before.txt | 97 + Last3Mon/72/answers/42-lab03-challenge2.json | 121 + Last3Mon/72/answers/42-lab03-challenge2.txt | 98 + .../72/answers/43-lab03-after-breaker.json | 113 + .../72/answers/43-lab03-after-breaker.txt | 93 + Last3Mon/72/answers/44-doctor.txt | 13 + .../answers/72-lab01-challenge-timings.json | 586 ++ .../answers/72-lab01-challenge2-budget.json | 586 ++ .../72/answers/72-lab02-challenge-fast.json | 852 +++ .../answers/72-lab02-challenge2-faqcache.json | 1050 +++ .../72/answers/72-lab03-challenge-health.json | 238 + .../answers/72-lab03-challenge2-breaker.json | 1319 ++++ Last3Mon/72/answers/LAB-TEST-RESULT.md | 6407 +++++++++++++++++ Last3Mon/72/tbench/.gitignore | 4 + Last3Mon/72/tbench/Doc/IMP-PLAN.md | 16 + Last3Mon/72/tbench/README.md | 38 + Last3Mon/72/tbench/pyproject.toml | 22 + Last3Mon/72/tbench/src/tbench/__init__.py | 1 + Last3Mon/72/tbench/src/tbench/app.py | 75 + Last3Mon/72/tbench/src/tbench/cli.py | 166 + Last3Mon/72/tbench/src/tbench/profiles.py | 37 + Last3Mon/72/tbench/src/tbench/runner.py | 67 + Last3Mon/72/tbench/src/tbench/stats.py | 69 + Last3Mon/72/tbench/src/tbench/store.py | 101 + Last3Mon/72/tbench/tests/conftest.py | 68 + Last3Mon/72/tbench/tests/test_app.py | 34 + Last3Mon/72/tbench/tests/test_engine.py | 129 + Last3Mon/72/tbench/uv.lock | 267 + 105 files changed, 24165 insertions(+) create mode 100644 Last3Mon/72/LAB-01-A-MEASURE-BOTTLENECK.md create mode 100644 Last3Mon/72/LAB-02-A-OPTIMIZE-FLOW.md create mode 100644 Last3Mon/72/LAB-03-A-STABILITY.md create mode 100644 Last3Mon/72/VIBE-A-TBENCH.md create mode 100644 Last3Mon/72/answers/00-lab01-baseline.txt create mode 100644 Last3Mon/72/answers/01-lab01-baseline.json create mode 100644 Last3Mon/72/answers/01-lab01-baseline.txt create mode 100644 Last3Mon/72/answers/02-lab01.json create mode 100644 Last3Mon/72/answers/02-lab01.txt create mode 100644 Last3Mon/72/answers/03-lab01-call.txt create mode 100644 Last3Mon/72/answers/04-lab01-kinds.txt create mode 100644 Last3Mon/72/answers/05-lab01-challenge-before.json create mode 100644 Last3Mon/72/answers/05-lab01-challenge-before.txt create mode 100644 Last3Mon/72/answers/06-lab01-challenge.json create mode 100644 Last3Mon/72/answers/06-lab01-challenge.txt create mode 100644 Last3Mon/72/answers/07-lab01-after-challenge.json create mode 100644 Last3Mon/72/answers/07-lab01-after-challenge.txt create mode 100644 Last3Mon/72/answers/08-lab01-challenge2-before.json create mode 100644 Last3Mon/72/answers/08-lab01-challenge2-before.txt create mode 100644 Last3Mon/72/answers/09-lab01-challenge2.json create mode 100644 Last3Mon/72/answers/09-lab01-challenge2.txt create mode 100644 Last3Mon/72/answers/10-lab01-challenge-after2.json create mode 100644 Last3Mon/72/answers/10-lab01-challenge-after2.txt create mode 100644 Last3Mon/72/answers/11-lab01-after2.json create mode 100644 Last3Mon/72/answers/11-lab01-after2.txt create mode 100644 Last3Mon/72/answers/12-lab02-rules.json create mode 100644 Last3Mon/72/answers/12-lab02-rules.txt create mode 100644 Last3Mon/72/answers/13-lab02-rules-bench.txt create mode 100644 Last3Mon/72/answers/14-lab02-cache.json create mode 100644 Last3Mon/72/answers/14-lab02-cache.txt create mode 100644 Last3Mon/72/answers/15-lab01-cache.json create mode 100644 Last3Mon/72/answers/15-lab01-cache.txt create mode 100644 Last3Mon/72/answers/16-cache-stats.txt create mode 100644 Last3Mon/72/answers/17-cache-hit-bench.txt create mode 100644 Last3Mon/72/answers/18-lab02-challenge-before.json create mode 100644 Last3Mon/72/answers/18-lab02-challenge-before.txt create mode 100644 Last3Mon/72/answers/19-bench-before.txt create mode 100644 Last3Mon/72/answers/20-lab02-challenge.json create mode 100644 Last3Mon/72/answers/20-lab02-challenge.txt create mode 100644 Last3Mon/72/answers/21-lab02-after-challenge.json create mode 100644 Last3Mon/72/answers/21-lab02-after-challenge.txt create mode 100644 Last3Mon/72/answers/22-lab01-after-challenge.json create mode 100644 Last3Mon/72/answers/22-lab01-after-challenge.txt create mode 100644 Last3Mon/72/answers/23-bench-after.txt create mode 100644 Last3Mon/72/answers/24-lab02-challenge2-before.json create mode 100644 Last3Mon/72/answers/24-lab02-challenge2-before.txt create mode 100644 Last3Mon/72/answers/25-lab02-challenge2.json create mode 100644 Last3Mon/72/answers/25-lab02-challenge2.txt create mode 100644 Last3Mon/72/answers/26-lab02-challenge-after2.json create mode 100644 Last3Mon/72/answers/26-lab02-challenge-after2.txt create mode 100644 Last3Mon/72/answers/27-lab02-after2.json create mode 100644 Last3Mon/72/answers/27-lab02-after2.txt create mode 100644 Last3Mon/72/answers/28-lab03-fault-before.txt create mode 100644 Last3Mon/72/answers/29-lab03.json create mode 100644 Last3Mon/72/answers/29-lab03.txt create mode 100644 Last3Mon/72/answers/30-lab02-resilient.json create mode 100644 Last3Mon/72/answers/30-lab02-resilient.txt create mode 100644 Last3Mon/72/answers/31-llm-down-before.txt create mode 100644 Last3Mon/72/answers/32-lab03-llm-down-before.json create mode 100644 Last3Mon/72/answers/32-lab03-llm-down-before.txt create mode 100644 Last3Mon/72/answers/33-lab03-llm-down.json create mode 100644 Last3Mon/72/answers/33-lab03-llm-down.txt create mode 100644 Last3Mon/72/answers/34-lab02-llm-fallback.json create mode 100644 Last3Mon/72/answers/34-lab02-llm-fallback.txt create mode 100644 Last3Mon/72/answers/35-lab03-llm-fallback.json create mode 100644 Last3Mon/72/answers/35-lab03-llm-fallback.txt create mode 100644 Last3Mon/72/answers/36-lab03-ticket-before.json create mode 100644 Last3Mon/72/answers/36-lab03-ticket-before.txt create mode 100644 Last3Mon/72/answers/37-lab03-ticket.json create mode 100644 Last3Mon/72/answers/37-lab03-ticket.txt create mode 100644 Last3Mon/72/answers/38-lab03-after-ticket.json create mode 100644 Last3Mon/72/answers/38-lab03-after-ticket.txt create mode 100644 Last3Mon/72/answers/39-lab03-llm-down-after-ticket.json create mode 100644 Last3Mon/72/answers/39-lab03-llm-down-after-ticket.txt create mode 100644 Last3Mon/72/answers/40-lab03-challenge.json create mode 100644 Last3Mon/72/answers/40-lab03-challenge.txt create mode 100644 Last3Mon/72/answers/41-lab03-challenge2-before.json create mode 100644 Last3Mon/72/answers/41-lab03-challenge2-before.txt create mode 100644 Last3Mon/72/answers/42-lab03-challenge2.json create mode 100644 Last3Mon/72/answers/42-lab03-challenge2.txt create mode 100644 Last3Mon/72/answers/43-lab03-after-breaker.json create mode 100644 Last3Mon/72/answers/43-lab03-after-breaker.txt create mode 100644 Last3Mon/72/answers/44-doctor.txt create mode 100644 Last3Mon/72/answers/72-lab01-challenge-timings.json create mode 100644 Last3Mon/72/answers/72-lab01-challenge2-budget.json create mode 100644 Last3Mon/72/answers/72-lab02-challenge-fast.json create mode 100644 Last3Mon/72/answers/72-lab02-challenge2-faqcache.json create mode 100644 Last3Mon/72/answers/72-lab03-challenge-health.json create mode 100644 Last3Mon/72/answers/72-lab03-challenge2-breaker.json create mode 100644 Last3Mon/72/answers/LAB-TEST-RESULT.md create mode 100644 Last3Mon/72/tbench/.gitignore create mode 100644 Last3Mon/72/tbench/Doc/IMP-PLAN.md create mode 100644 Last3Mon/72/tbench/README.md create mode 100644 Last3Mon/72/tbench/pyproject.toml create mode 100644 Last3Mon/72/tbench/src/tbench/__init__.py create mode 100644 Last3Mon/72/tbench/src/tbench/app.py create mode 100644 Last3Mon/72/tbench/src/tbench/cli.py create mode 100644 Last3Mon/72/tbench/src/tbench/profiles.py create mode 100644 Last3Mon/72/tbench/src/tbench/runner.py create mode 100644 Last3Mon/72/tbench/src/tbench/stats.py create mode 100644 Last3Mon/72/tbench/src/tbench/store.py create mode 100644 Last3Mon/72/tbench/tests/conftest.py create mode 100644 Last3Mon/72/tbench/tests/test_app.py create mode 100644 Last3Mon/72/tbench/tests/test_engine.py create mode 100644 Last3Mon/72/tbench/uv.lock diff --git a/Last3Mon/72/LAB-01-A-MEASURE-BOTTLENECK.md b/Last3Mon/72/LAB-01-A-MEASURE-BOTTLENECK.md new file mode 100644 index 0000000..7975e5c --- /dev/null +++ b/Last3Mon/72/LAB-01-A-MEASURE-BOTTLENECK.md @@ -0,0 +1,117 @@ +# Lab 01 · 재고, 단계별 시간을 남기고, 병목 찾기 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-01-Q-MEASURE-BOTTLENECK.md](LAB-01-Q-MEASURE-BOTTLENECK.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini). +> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). + +## 예제 1 · 기준 + +| 요청 | 결과 | +| --- | --- | +| `base-helpdesk` push 직후 첫 호출 (`faq-vpn`) | 6.5초 | +| `bench base-helpdesk requests/faq-vpn.json -n 5` | p50 2.61초 · p95 2.83초 · 실패 0 | +| `call base-helpdesk` `ticket-own` · `off-topic` | 3.3초 · 3.1초 | + +- `off-topic`(점심 메뉴)도 LLM을 세 번 부르고 `route: 자동 답변`으로 "찾지 못했습니다"를 답합니다. 3단계 대체 경로와 2단계 규칙에서 다시 이야기합니다. + +## 예제 2 · 요청문 예 + +```text +my-work/helpdesk.json 은 reference/baseline-helpdesk.json 을 복사한 것이야. 웹훅 경로를 my-helpdesk, 이름을 "72 IT helpdesk (my)"로 바꾸고, +답은 그대로 두고 결과에 다음을 더해 줘 (AGENTS.md 3절, 5절의 시각 남기기). +- Has question? 다음에 Start clock, Parse category 뒤에 After classify, Get ticket 뒤에 After data, Answer 뒤에 After answer, Review 뒤에 After review (각각 Date.now()). +- Build result 에서 timings {classify_ms, data_ms, answer_ms, review_ms, total_ms}, steps(지나간 단계), llm_calls(지나간 Agent 수), cached:false. + 지나갔는지는 $('노드').isExecuted 로. +기대 결과 cases/lab01.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `After data` | 다른 시각 노드처럼 "After + 끝난 단계". 데이터(FAQ · 접수 조회)가 끝난 뒤 | +| ② | `Date.now()` | 지금 시각을 ms 숫자로 (AGENTS.md 5절 `Start clock` 예) | +| ③ | `data_ms` | `timings` 칸 이름 규칙 `<단계>_ms` (AGENTS.md 3절) | +| ④ | `$('노드').isExecuted` | 노드가 실행됐으면 true (AGENTS.md 5절) | + +정답: [reference/lab01-example2-timings.json](reference/lab01-example2-timings.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab01.json` | 4/4 (기준 `--webhook base-helpdesk`는 1/4) | +| `faq-vpn`의 `timings` | classify 524 · data 21 · answer 1017 · review 919 · total 2500 ms (다른 실행: 565 · 23 · 1063 · 879 · 2546) | + +- LLM 세 번이 전체의 약 99%. 데이터 API 두 번은 20ms대. +- 10/24 다시 돌린 결과: `lab01` **3/4** — 접수 케이스에서 검토 LLM이 "처리 중"을 "승인 대기 중"으로 바꿔 써 실패 (정답의 잘못이 아님). VPN `timings` 616 · 18 · 1124 · 920 · 2698ms. + +## 예제 3 · 결과 (에이전트 없이, 10/24) + +예제 2 정답(`reference/lab01-example2-timings.json`) 그대로, 캐시 없음. + +| 질문 종류 | p50 | p95 | classify | data | answer | review | total | LLM 몫 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| FAQ (VPN) | 2.91초 | 3.08초 | 616 | 18 | 1124 | 920 | 2698 | 98.6% | +| 접수 번호 (T-1002) | 2.99초 | 3.12초 | 818 | 43 | 877 | 895 | 2647 | 97.8% | +| IT 밖 (점심 메뉴) | 2.48초 | 2.84초 | 732 | 19 | 740 | 978 | 2482 | 98.7% | + +(p50 · p95는 `bench -n 5`, 단계 칸은 `call -v` 한 번의 ms. FAQ 줄의 단계 칸은 예제 2 `call` 값) + +- 학생이 쓸 한 줄 예: "세 종류 모두 LLM이 98%. 점심 메뉴는 데이터가 없는데도 LLM 세 번 → 규칙으로 걸러 낼 후보." +- 같은 실행에서 `lab01` 접수 케이스가 한 번 실패했습니다 (검토 LLM이 "처리 중"을 "승인 대기 중"으로 바꿈). Lab 02 과제 1의 이야기 거리. + +## 과제 1 · 요청문 예 + +```text +my-work/helpdesk.json 의 Search FAQ 와 Get ticket 사이에 After FAQ(Date.now()) 를 더해, timings 의 data_ms 를 faq_ms 와 ticket_ms 로 나눠 줘. +total_ms 를 뺀 timings 중 가장 큰 단계 이름(_ms 뺀 것)을 slowest_step 으로. cases/lab01-challenge.json, cases/lab01.json 둘 다 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `After FAQ` | FAQ 검색이 끝난 뒤의 시각 노드 | +| ② | `faq_ms` | `cases/lab01-challenge.json`의 `expect` 칸 이름 | +| ③ | `ticket_ms` | ②와 짝. `data_ms`를 FAQ와 접수 조회로 나눔 | +| ④ | 가장 큰 | "가장 느린" = `_ms` 값이 가장 큰 칸 | + +정답: [answers/72-lab01-challenge-timings.json](answers/72-lab01-challenge-timings.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab01-challenge.json` | 4/4 (예제 2 상태 0/4) | +| `cases/lab01.json` | 4/4 | +| `faq-vpn` | classify 565 · faq 14 · ticket 9 · answer 1063 · review 879 · total 2546 ms, `slowest_step: answer` | + +10/24 다시 돌린 결과: `lab01-challenge` 4/4, `slowest_step`은 네 번 중 answer 2 · review 1 · classify 1 (push 직후 첫 호출에서 classify 1365ms). + +## 과제 2 · 요청문 예 + +```text +my-work/helpdesk.json 의 Build result 에서, 본문 budget_ms (없으면 3000, 숫자로)를 읽어 +결과에 llm_ms(classify · answer · review 의 _ms 합, timings 밖에 둔다), budget_ms, over_budget(timings.total_ms > budget_ms)을 더해 줘. +timings, slowest_step 은 그대로. cases/lab01-challenge2.json, cases/lab01-challenge.json, cases/lab01.json 모두 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `budget_ms` | 본문 입력 칸 = 결과 칸 이름 (AGENTS.md 3절 "입력") | +| ② | `3000` | 기본 예산 3초 (조건 표 · AGENTS.md 3절) | +| ③ | 밖 | `timings` 안에 두면 `slowest_step`이 `llm` 합계를 고르게 됨 | +| ④ | `timings.total_ms` | 요청 하나 전체 시간 | + +정답: [answers/72-lab01-challenge2-budget.json](answers/72-lab01-challenge2-budget.json) (과제 1 정답의 `Build result`에 세 줄) + +| 10/24 결과 | | +| --- | --- | +| 고치기 전 (과제 1 정답) | `lab01-challenge2` 0/4 (칸이 없음) | +| 고친 뒤 | `lab01-challenge2` 4/4 · `lab01-challenge` 4/4 · `lab01` 4/4 | +| 예산 60000 | `total_ms` 3357 (push 직후) · `llm_ms` 3295 · `over_budget: false` | +| 예산 500 | 2609 · 2563 · `true` | +| 기본 3000 (VPN) | 3023 · 2973 · **`true`** (23ms 넘음) | +| 기본 3000 (접수) | 2904 · 2858 · `false` | + +- 기본 예산에서 `over_budget`이 실행마다 바뀌는 것이 정상입니다 (VPN이 3초 안팎). 풀이 때 "그래서 2단계에서 줄인다"로 잇습니다. +- 처음 만든 케이스 파일은 접수 케이스에 답 "처리 중"을 기대했는데, 검토 LLM이 문장을 바꿔 실패했습니다 (10/24). 모양을 보는 테스트이므로 `category: ticket`으로 바꿨습니다. diff --git a/Last3Mon/72/LAB-02-A-OPTIMIZE-FLOW.md b/Last3Mon/72/LAB-02-A-OPTIMIZE-FLOW.md new file mode 100644 index 0000000..ac8cdf9 --- /dev/null +++ b/Last3Mon/72/LAB-02-A-OPTIMIZE-FLOW.md @@ -0,0 +1,143 @@ +# Lab 02 · 규칙 먼저, 캐시, LLM 한 번 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-02-Q-OPTIMIZE-FLOW.md](LAB-02-Q-OPTIMIZE-FLOW.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini, bench는 n=5). +> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). 10/24 점수: 예제 1 `lab02` 7/9 (캐시 케이스 · 접수 답에서 검토 LLM이 "처리 중"을 바꿈), 나머지는 모두 기대대로. + +## 예제 1 · 요청문 예 + +```text +my-work/helpdesk.json 의 순서를 바꿔 줘: Has question? → Start clock → Search FAQ → Get ticket → After data → Rule classify(Code) → Known?(If). +Rule classify: 질문에 T-1234 같은 번호(/T-?\d{4}/i)가 있으면 ticket, FAQ 결과가 1개 이상이면 faq, 아니면 unknown. {category, by:'rule'}. +Known? true → Answer, false → Classify → Parse category → After classify → Answer. Answer 는 $json.category 로 분류를 읽는다. +Build result: 분류는 Parse category 가 실행됐으면 그것, 아니면 Rule classify. steps 에 rule 또는 classify. +나머지(답, 검토, timings)는 그대로. 기대 결과 cases/lab02.json 앞 네 케이스. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `/T-?\d{4}/i` | 하이픈 있어도 없어도, 대소문자 무시, 숫자 네 자리 (`T-1002` · `t1002`) | +| ② | 1개 이상 | `/faq`의 `results`가 비어 있지 않으면 FAQ로 답할 수 있음 | +| ③ | `Parse category` | LLM 분류 길에서 분류 결과를 만드는 마지막 노드 | +| ④ | `rule` | 규칙 길을 지났다는 `steps` 이름 (AGENTS.md 3절) | + +정답: [reference/lab02-example1-rules.json](reference/lab02-example1-rules.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab02.json` | 7/9 (예제 2의 "같은 질문" 캐시 케이스와, 답 문장이 기대 표현과 달랐던 "없는 접수 번호" 실패. 10/23~24에는 "없는 접수 번호"가 통과해 8/9) | +| `cases/lab01.json` | 4/4 | +| 규칙 질문 시간 | 접수 1.9~2.3초 (예제 2 timings 상태에서 2.4~2.8초) | + +- 처음 쓴 "없는 접수 번호" 기대(`찾지 못`)가 틀렸습니다: 검토 LLM이 "확인되지 않습니다"로 바꿔 말함. 기대를 "없다는 여러 표현 + 상태를 지어내지 않음(`not_contains` 처리 중 등)"으로 고쳤습니다 (71일차 "모른다의 여러 모양"과 같음). + +## 예제 2 · 요청문 예 + +```text +my-work/helpdesk.json 에 캐시를 더해 줘 (AGENTS.md 2절 데이터 API, 3절 캐시 규칙). +Start clock 다음: Cache key(Code: question 을 trim·소문자·연속 공백 하나, use_cache = body.no_cache !== true) +→ Cache lookup(GET http://data-api:8800/cache/, neverError) → Cached?(value 있고 use_cache) + true → Cached result(route·category·answer·sources + llm_calls 0, cached true, steps ['cache'], timings.total_ms) → Send cached + false → 지금 흐름(Search FAQ ...). +Build result 뒤: Cache it?(category faq 이고 use_cache) true → Store in cache(POST /cache/ {value:{route,category,answer,sources}, ttl 600}) → Send result. +접수(ticket)는 저장하지 않는다. 기대 결과 cases/lab02.json 전체, lab01 도 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | trim · 소문자 · 연속 공백 하나 | 띄어쓰기 · 대소문자만 다른 질문을 같은 키로 (AGENTS.md 3절) | +| ② | `body.no_cache !== true` | 본문 `no_cache`가 true가 아닐 때만 캐시 사용 | +| ③ | `neverError` | 캐시에 없을 때의 404에서 멈추지 않게 (AGENTS.md 5절) | +| ④ | 접수(ticket) | 상태가 바뀌는 답은 캐시하지 않음 (AGENTS.md 3절 굵은 줄) | + +정답: [reference/lab02-example2-cache.json](reference/lab02-example2-cache.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab02.json` | 9/9 · 같은 질문(띄어쓰기·대소문자 다름) 0.1초, `llm_calls: 0` | +| `cases/lab01.json` · `lab03.json` | 4/4 · 2/4 (3단계 전) | +| 캐시 없이 (`faq-vpn-nocache`) bench | p50 2.01초 · p95 2.23초 | + +- 처음에는 "규칙으로 모르는 질문"(회의실 화면 공유)이 LLM 분류로 `faq`가 되어 **캐시에 들어갔고**, 다음 테스트에서 0.1초로 나와 `llm_calls` 기대가 깨졌습니다. 그 케이스에 `no_cache: true`를 넣었습니다 → 수업 이야기 거리: "캐시는 테스트도 바꾼다". + +## 예제 3 · 결과 (에이전트 없이, 10/24) + +예제 2 정답(`reference/lab02-example2-cache.json`), 캐시를 비운 뒤. + +| 재기 | 첫 호출 | p50 | max | +| --- | --- | --- | --- | +| 캐시 씀 (`faq-vpn` × 5) | 2.07초 (LLM 2번) | **0.05초** | 2.07초 | +| 캐시 안 씀 (`faq-vpn-nocache` × 5) | 1.99초 | 1.99초 | 2.24초 | + +- `/cache-stats` 뒤: `entries 1 · hits 9 · misses 1`. `no_cache` 요청도 캐시를 **조회는** 하므로 hits로 셉니다 (쓰지는 않음). 학생이 "적중 9번?"이라고 하면 이 이야기를 합니다. +- 적중률 어림 (0.05초 · 2.0초): 10% → 1.81초, 30% → 1.42초, 60% → 0.83초. + +## 과제 1 · 요청문 예 + +```text +my-work/helpdesk.json 에서 Review(검토 LLM)와 Review model 을 지우고, 그 자리에 Review check(Code)를 둬: +Answer 의 output 을 trim, 비었으면 empty, 한글이 없으면 not_korean, 400자 넘으면 자르고 too_long 을 review_problems 에. +Answer 지시문에 "합니다체, 3문장 이내"가 이미 있는지 확인. steps 에는 review 대신 check, llm_calls 는 검토를 세지 않는다. +나머지는 그대로. cases/lab02-challenge.json, lab02.json, lab01.json 모두 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `400` | 답 길이 상한 (교안 2단계 6절 검사 표) | +| ② | 합니다체, 3문장 이내 | 검토 LLM이 하던 말투 · 길이 다듬기를 지시문으로 | +| ③ | `check` | 검토 대신 코드 검사를 지났다는 단계 이름 (다른 짧은 이름도 됨) | +| ④ | 검토를 세지 않는다 | 지운 Review는 빼고 셈: 규칙 길 1, LLM 분류 길 2 | + +정답: [answers/72-lab02-challenge-fast.json](answers/72-lab02-challenge-fast.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab02-challenge.json` | 5/5 (VPN 1.2초 · 비밀번호 1.1초 · 접수 1.3초) · 캐시 단계 상태 1/5 | +| `cases/lab02.json` · `lab01.json` | 9/9 · 4/4 | +| 캐시 없이 bench | p50 2.01 → **1.15초** · p95 2.23 → **1.16초** | + +- 처음 쓴 `lab01` · `lab02` 기대에 `llm_calls: 3`, `steps`에 `review` 같은 **구조**가 들어 있어, 과제로 정당하게 빨라진 워크플로우가 실패했습니다. 품질 테스트는 **답의 내용**만 보게 고쳤습니다 (구조 기대는 그 단계의 테스트에만) → 이야기 거리: "최적화하면 깨지는 테스트는 무엇을 재고 있었나". + +10/24 다시 돌린 결과: 고치기 전 `lab02-challenge` 1/5, bench p50 2.22 · p95 2.45초 → 고친 뒤 15/15 (`--repeat 3`, 0.9~1.6초), `lab02` 9/9, `lab01` 4/4, bench p50 **1.12** · p95 1.32초. + +## 과제 2 · 요청문 예 + +```text +my-work/helpdesk.json 에 FAQ 번호 캐시를 더해 줘 (AGENTS.md 3절 캐시 키 표). +Rule classify 다음: FAQ cache key(Code: category 가 faq 이고 use_cache 이고 FAQ 결과가 있으면 faq_key = 'faq-' + 첫 FAQ id, 아니면 'none') +→ FAQ cache lookup(GET /cache/, neverError) → FAQ cached?(value 있고 faq_key 가 'none' 아님) + true → FAQ cached result(llm_calls 0, cached true, cache_by 'faq', steps ['faq', 'rule', 'cache']) → Send + false → Back to rule(Code: Rule classify 의 결과를 그대로) → Known? +Store in cache 뒤에 Store by FAQ(POST /cache/, 같은 value, ttl 600) → Result again. +질문 캐시의 Cached result 에는 cache_by 'question', Build result 에는 cache_by null. +기대 결과 cases/lab02-challenge2.json, lab02-challenge.json, lab02.json 도 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `faq-` | 캐시 키 표 둘째 줄: `faq-` | +| ② | `faq` | FAQ 번호 키로 찾았을 때 (AGENTS.md 3절 Lab 02 과제 2) | +| ③ | `600` | 질문 캐시와 같은 ttl (초) | +| ④ | `question` | ②와 짝. 질문 키로 찾았을 때 | + +정답: [answers/72-lab02-challenge2-faqcache.json](answers/72-lab02-challenge2-faqcache.json) (과제 1 정답에 노드 7개: FAQ cache key · FAQ cache lookup · FAQ cached? · FAQ cached result · Send FAQ cached · Back to rule · Store by FAQ) + +| 10/24 결과 (`cases/lab02-challenge2.json`) | 고치기 전 (과제 1 정답) | 고친 뒤 | +| --- | --- | --- | +| 처음 묻는 VPN | ✔ 1.1초 | ✔ 2.5초 (push 직후) | +| 다른 말 VPN | ✘ `cached: false`, LLM 1번, 1.2초 | ✔ `cache_by: faq`, LLM 0번, **0.1초** | +| 같은 말 | ✘ `cache_by` 칸 없음 (캐시는 맞음, 0.1초) | ✔ `cache_by: question`, 0.0초 | +| `no_cache` | ✔ | ✔ 1.2초 | +| 프린터 처음 · 접수 현황 | ✔ | ✔ | +| 합 | 4/6 | 6/6, `lab02-challenge` 5/5, `lab02` 9/9 | + +- `Back to rule`이 없으면 `Known?`이 캐시 응답을 읽어 분류가 사라집니다. 학생이 가장 많이 막힐 곳으로 예상합니다 (수업 준비 때 예상). +- 풀이 때 "잃는 것": FAQ-003에 묶인 질문은 모두 처음 질문에 맞춘 답을 받습니다. diff --git a/Last3Mon/72/LAB-03-A-STABILITY.md b/Last3Mon/72/LAB-03-A-STABILITY.md new file mode 100644 index 0000000..7adce05 --- /dev/null +++ b/Last3Mon/72/LAB-03-A-STABILITY.md @@ -0,0 +1,164 @@ +# Lab 03 · 멈춰도 답하게 + +> **강사용 정답본** · 학생 저장소에 올리지 않습니다. 학생용 문제는 [LAB-03-Q-STABILITY.md](LAB-03-Q-STABILITY.md). +> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 정답 워크플로우를 2026-10-18에 1회씩 실행한 값입니다 (강사 Mac, gpt-4.1-mini). +> 10/24에 실습을 5개로 늘리며 정답을 학생 순서대로 다시 돌렸습니다 (45단계). 명령 · `-v` 출력 전부와 codex 프롬프트: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md). + +## 예제 1 · 고치기 전 (캐시 단계 워크플로우) + +| 상황 | 결과 | +| --- | --- | +| FAQ 느림 (8초) | **9.0초** 기다린 뒤 `자동 답변` (사용자는 9초를 기다림) | +| FAQ 오류 (500) | 0.1초 만에 **HTTP 500** `Error in workflow` | +| 캐시에 있는 질문 + FAQ 오류 | 0.1초, `cached: true` (캐시 확인이 FAQ 앞에 있어서 이미 됨) | +| `cases/lab03.json` | 2/4 | + +## 예제 1 · 요청문 예 + +```text +my-work/helpdesk.json 의 Search FAQ 와 Get ticket 에 options.timeout 2000, 노드 설정 retryOnFail true, maxTries 2, waitBetweenTries 500, +onError continueErrorOutput 을 넣어 줘. 두 노드의 오류 출력(main 1번)은 Data down(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason data_api_down) +→ Send data down(Respond). Get ticket 은 404 를 받아야 하니 neverError 는 그대로 둔다. 기대 결과 cases/lab03.json, lab02 도 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `2000` | 2초를 ms로 | +| ② | `2` | 처음 + 다시 한 번. 최악 2 × 2000 + 500 = 4.5초로 느림 케이스 안 | +| ③ | `continueErrorOutput` | 실패하면 두 번째(오류) 출력으로 (AGENTS.md 5절) | +| ④ | `neverError` | 404(없는 번호)를 보통 결과로 받기 | + +정답: [reference/lab03-example1-resilient.json](reference/lab03-example1-resilient.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| `cases/lab03.json` | 4/4 · FAQ 느림 **4.6초** `담당자 연결` · FAQ 오류 **0.6초** `담당자 연결` · 캐시 0.1초 | + +- **바로잡음 (10/24):** 수업 준비 때 여기에 "`neverError` 때문에 접수 API 500이어도 실패로 보지 않는다"고 적었는데, 시험 워크플로우로 재 보니 이 n8n(2.42.6)에서 `neverError`는 **4xx만** 보통 결과로 받고 500은 재시도 뒤 오류 출력으로 갑니다 ([AGENTS.md](AGENTS.md) 5절 표). 대신 404도 재시도를 해서, 이 정답에서 VPN 질문의 `data_ms`가 605ms입니다 (예제 2 단계 32~76ms). 둘 다 예제 3에서 다룹니다. + +## 예제 2 · 요청문 예 + +```text +my-work/helpdesk.json 의 Classify 와 Answer Agent 에 onError continueErrorOutput 을 넣고, 두 노드의 오류 출력을 Simple answer(Code) → Send simple answer 로 이어 줘. +Simple answer: 질문에 접수 번호가 있고 Get ticket 결과에 status 가 있으면 route 간단 답변, "<번호> 접수 건은 현재 '' 상태입니다. ". +FAQ 결과가 있으면 route 간단 답변, 첫 FAQ answer + " (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", sources [그 id]. +아니면 route 담당자 연결. 모두 llm_calls 0, reason llm_down. 기대 결과 cases/lab03-llm-down.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `간단 답변` | LLM 없이 데이터 그대로 답하는 `route` (두 곳 같음) | +| ② | 자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다. | 평소 답이 아니라는 안내 (문장은 학생이 정해도 됨) | +| ③ | `담당자 연결` | 데이터로도 답할 수 없을 때 사람에게 | +| ④ | `llm_down` | AGENTS.md 3절 Lab 03 `reason` 중 LLM이 멈춘 경우 | + +정답: [reference/lab03-example2-llm-fallback.json](reference/lab03-example2-llm-fallback.json) + +| 수업 준비 때 결과 | | +| --- | --- | +| 고치기 전 (`lab03-llm-down`) | 1/4 · 세 질문 모두 **HTTP 500** (빈 질문 400만 통과) | +| 고친 뒤 | 4/4 · VPN 2.4초 `간단 답변` · 접수 0.3초 `간단 답변` · 점심 1.1초 `담당자 연결` | +| `llm up` 뒤 `lab03` | 4/4 | + +- `flow.py llm down`은 `Last3Mon/65/n8n/register-credentials.sh`를 틀린 키로 실행합니다. n8n 재시작 없이 바로 적용되고, `llm up`도 바로 돌아왔습니다. + +## 예제 3 · 요청문 예 + +```text +my-work/helpdesk.json 의 Get ticket 응답 설정에 fullResponse 와 neverError 를 켜 줘 (결과가 {statusCode, body}). +Get ticket 보통 출력 다음에 Ticket check(Code): ticket = statusCode 500 이상이면 {} 아니면 body, ticket_status = statusCode, +tickets_down = statusCode 500 이상이고 질문에 접수 번호(/T-?\d{4}/i)가 있을 때만 true. +→ Tickets down?(If) true → Tickets down(Set: route 담당자 연결, category ticket, answer "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", reason tickets_down, llm_calls 0) → Send tickets down + false → 지금 흐름(After data …). +$('Get ticket').first().json 을 읽던 곳은 모두 $('Ticket check').first().json.ticket 으로 바꾼다. 기대 결과 cases/lab03-ticket.json, lab03.json, lab03-llm-down.json 도 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `fullResponse` | `{statusCode, body}`를 통째로 받기 (AGENTS.md 5절 표 셋째 줄) | +| ② | `500 이상` | 서버 고장 범위. 404는 고장이 아님 (두 곳 같음) | +| ③ | `tickets_down` | AGENTS.md 3절 `reason` 중 접수 API만 멈춘 경우 | +| ④ | `$('Ticket check').first().json.ticket` | 이제 접수 건은 `Ticket check`의 `ticket` 칸에 | + +정답: [reference/lab03-example3-ticket-down.json](reference/lab03-example3-ticket-down.json) (예제 2 정답에 `fullResponse` + 노드 4개: Ticket check · Tickets down? · Tickets down · Send tickets down) + +| 10/24 결과 (`cases/lab03-ticket.json`) | 고치기 전 (예제 2 정답) | 고친 뒤 | +| --- | --- | --- | +| 평소 접수 현황 | ✔ | ✔ 2.4초 | +| 접수 API 500 · 접수 질문 | ✘ `reason: data_api_down` (0.6초) | ✔ `tickets_down`, 0.1초 | +| 접수 API 500 · VPN 질문 | ✘ **`담당자 연결`** (0.6초) | ✔ `자동 답변`, 1.1초, "인증서" | +| 없는 번호 (404) | ✔ | ✔ 1.3초 | +| 합 | 2/4 | 4/4, `lab03` 4/4, `lab03-llm-down` 4/4 | + +- 고친 뒤 VPN 질문의 `data_ms` 38ms (예제 1 정답 605ms). 404 재시도가 사라졌기 때문입니다. +- 풀이 때 짚을 것: "오류 출력은 어느 질문인지 모른다" → 실패를 보통 결과로 받아 워크플로우가 판단. + +## 과제 1 · 요청문 예 + +```text +my-work/health.json 을 새로 만들어 줘 (웹훅 my-health, AGENTS.md 3절). +Start(Code t0) → Check data API(GET http://data-api:8800/health, timeout 2000, onError continueRegularOutput, alwaysOutputData) +→ Check FAQ(GET /faq?q=vpn, 같은 설정) → Check LLM(Agent "Reply with the single word OK.", onError continueRegularOutput, alwaysOutputData; + 모델 options timeout 8000, maxRetries 0) → Summarize(Code): data_api = status ok, faq = results 가 배열, llm = output 에 ok. + failed = 거짓인 것 이름. status: 모두 참 ok, data_api 나 llm 중 하나라도 참이면 degraded, 아니면 down. seconds. +기대 결과 cases/lab03-challenge.json. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `continueRegularOutput` | 실패해도 **보통** 출력으로 다음 점검에 (두 곳 같음) | +| ② | `status` 가 `ok` | `/health`가 평소 돌려주는 `{"status":"ok"}` | +| ③ | `output` 에 `ok` | "OK."처럼 대문자 · 마침표가 붙어도 참이 되게 소문자로 비교 | +| ④ | `data_api` 나 `llm` 중 하나라도 참이면 `degraded` | 일부는 된다 = 둘 중 하나는 살아 있음 (학생 기준을 적어도 됨) | + +정답: [answers/72-lab03-challenge-health.json](answers/72-lab03-challenge-health.json) + +| 수업 준비 때 결과 (`cases/lab03-challenge.json` 4/4) | | +| --- | --- | +| 평소 | `ok`, 2.2초 | +| FAQ 오류 | `degraded`, `checks.faq: false`, 0.8초 | +| FAQ 느림 (8초) | `degraded`, 2.8초 (timeout 2초에서 끊고 계속) | +| LLM 멈춤 | `degraded`, `checks.llm: false`, 0.6초 | + +10/24 다시 돌린 결과: `lab03-challenge` 4/4 · 평소 `ok` 2.2초 · FAQ 오류 `degraded` 1.0초 · FAQ 느림 3.0초 · LLM 멈춤 0.4초. + +## 과제 2 · 요청문 예 + +```text +my-work/helpdesk.json 에 FAQ 차단기를 더해 줘 (AGENTS.md 3절 캐시 키 표). +Cached? false → Breaker check(GET http://data-api:8800/cache/breaker-faq, neverError) → Breaker open?(value 있음) + true → Breaker open(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason breaker_open, llm_calls 0) → Send breaker open + false → Search FAQ. +Search FAQ 의 오류 출력 → Open breaker(POST /cache/breaker-faq {value:{open:true, since: Date.now()}, ttl 30}, neverError) → Data down. +닫는 노드는 만들지 않는다 (ttl 30초가 지나면 데이터 API가 지움). 기대 결과 cases/lab03-challenge2.json, lab03.json 도 통과. +``` + +**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`) + +| 빈칸 | 답 | 왜 | +| --- | --- | --- | +| ① | `breaker-faq` | 캐시 키 표 마지막 줄 (두 곳 같음) | +| ② | `breaker_open` | AGENTS.md 3절 `reason` 중 차단기가 열린 경우 | +| ③ | `30` | 초. 조건 표 · 캐시 키 표의 ttl | +| ④ | `Data down` | 이번 사용자에게는 예제 1의 정해진 답 | + +정답: [answers/72-lab03-challenge2-breaker.json](answers/72-lab03-challenge2-breaker.json) (예제 3 정답에 노드 5개: Breaker check · Breaker open? · Breaker open · Send breaker open · Open breaker) + +| 10/24 결과 (`cases/lab03-challenge2.json`) | 고치기 전 (예제 3 정답) | 고친 뒤 | +| --- | --- | --- | +| 평소 | ✔ 1.3초 | ✔ 2.7초 (push 직후) | +| FAQ 느림 · 처음 | ✔ 4.6초 `data_api_down` | ✔ 4.8초 `data_api_down` | +| FAQ 느림 · 다음 질문 | ✘ **4.6초** `data_api_down` | ✔ **0.1초** `breaker_open` | +| 되살아남 (캐시 비움) | ✔ 1.2초 | ✔ 1.1초 | +| 합 | 3/4 | 4/4, `lab03` 4/4 | + +- 손으로 30초 닫힘을 보여 줄 때: `fault faq slow` → `call` 둘 → `curl -s localhost:8800/cache/breaker-faq` → 30초 뒤 `call` (다시 4초대, 다시 열림) → `fault all ok`. +- 이야기 거리: 차단기 상태를 워크플로우 밖(캐시)에 둔 까닭 — 웹훅 실행은 요청마다 새로 시작. diff --git a/Last3Mon/72/VIBE-A-TBENCH.md b/Last3Mon/72/VIBE-A-TBENCH.md new file mode 100644 index 0000000..f40aed2 --- /dev/null +++ b/Last3Mon/72/VIBE-A-TBENCH.md @@ -0,0 +1,197 @@ +# 7교시 바이브 코딩 · `tbench` 만들기 + +> **정답본** · 수업이 끝난 뒤(8교시 뒤) 학생 저장소에 올립니다. 그 전에는 올리지 않습니다. 요청서는 [VIBE-REQ-TBENCH.md](VIBE-REQ-TBENCH.md), 문제는 [VIBE-Q-TBENCH.md](VIBE-Q-TBENCH.md). +> 68일차부터 바이브 코딩 프로그램은 **수업 준비 때 미리 만들어 보지 않습니다.** 아래 TODO · SPEC · IMP-PLAN · 요청문과 확인 기준은 예시이고, 걸린 시간은 8교시 강사 바이브 코딩에서 재어 기록합니다. +> **10/26 추가:** 강사 요청으로 R1~R14를 모두 넣은 본을 [tbench/](tbench/)에 직접 만들어 봤습니다 (7절). 요청문으로 CLI 에이전트에 시킨 것이 아니라 Claude Code가 SPEC · IMP-PLAN을 보고 쓴 것이라, 8교시 걸린 시간 표와는 따로 봅니다. + +## 1. `Doc/TODO.md` 예 + +```markdown +# tbench TODO + +## 만들고 싶은 것 +어느 폴더에서나 `tbench`로 웹훅을 여러 번 불러, 보통 · 느린 쪽 시간과 실패를 보여 주고, +바꾸기 전 · 후를 같은 조건으로 비교하는 터미널 도구. 나중에는 기준을 주면 통과 · 실패만 알려 줌. + +## 요청의 말 → 할 일 +| # | 요청의 말 (짧게) | 할 일 (내 말) | 꼭 · 나중 | +| --- | --- | --- | --- | +| ① | 여러 번 · 몇 번 실패 · 보통 · 느린 쪽 | N번 부르고 한 번마다 상태 · 시간, 끝에 p50 · p95 · max · 실패 | 꼭 | +| ② | 기다리기 싫음 · 모양 | 오는 대로 표에 한 줄씩, 스파크라인 | 꼭 | +| ③ | 숫자로 비교 · 다른 질문 비교 망신 | 결과 저장, 두 결과 비교, 조건이 다르면 경고 | 꼭 | +| ④ | 한꺼번에 | 동시에 k개 | 나중 | +| ⑤ | 만들어 둔 요청 고르기 | requests 폴더에서 고르기 (turl은 형식 확인 뒤) | 나중 | +| ⑥ | 첫 번만 느림 | 첫 호출 표시, 빼고도 p50, 미리 불러 두기 | 꼭 | +| ⑦ | 왜 실패했는지 | 연결 · 시간 초과 · 서버 오류를 따로 셈 | 꼭 | +| ⑧ | 단계별 시간 평균 | 응답의 timings를 모아 단계 평균 | 나중 | +| ⑨ | 화면 없이 통과 · 실패 | 기준을 넘으면 실패, 종료 코드 | 나중 | +| ⑩ | 줄줄이 기록 | 한 번마다 한 줄 파일 | 나중 | +| ⑪ | 설정 저장 · 이름으로 | 설정 파일, 이름으로 실행 | 나중 | +| ⑫ | 잰 것 목록 | 저장한 결과 목록 | 나중 | +| 끝 | 키가 찍히면 안 됨 | 저장 · 기록에서 키 가리기 | 꼭 | +| 끝 | 진짜 헬프데스크 두드리지 않기 | 시험은 가짜 서버로 | 꼭 | +| 첫 문단 | 어느 폴더에서나 | uv tool install | 꼭 | + +## 요청에 없지만 걱정되는 것 +- 시간을 어디서부터 어디까지 재나, 끝없이 기다리면? +- p95를 어떻게 계산하나 (사람마다 다르면 비교가 안 됨) +- 저장 폴더 · 설정 파일을 어디에 (어느 폴더에서 실행해도 같은 곳) + +## 오늘 50분 안에 못 할 것 같은 것 +④ ⑤ ⑧ ⑨ ⑩ ⑪ ⑫ — IMP-PLAN의 다음 단계로 +``` + +## 2. `Doc/SPEC.md` 예 (요구 표는 VIBE-Q와 같음, 확인 기준을 구체적으로) + +| 요구 | 확인 기준 | +| --- | --- | +| R1 | 가짜 서버(지연 0.1초, 3번째마다 500)로 n=6 → 실패 2, 시간 6개. `stats.py` 테스트: 1~10초 → p50 5 · p95 10 (nearest-rank, 자리 = 올림(p × n)) | +| R2 | worker로 부름, 결과마다 표 한 줄 · 스파크라인 · 요약 갱신 (가짜 서버 지연 0.5초 × 6에서 3초 동안 한 줄씩 늘어남) | +| R3 | 저장 JSON: `{url, method, body_sha256, n, concurrency, warmup, started, seconds:[...], statuses:[...], errors:[...]}`. compare에서 url · body_sha256 · n이 다르면 `WARNING: different ...` 줄 | +| R4 | 가짜 서버 지연 0.3초 × 6번: `--concurrency 1` 약 1.8초, `3` 약 0.6초 안팎 | +| R5 | `requests/*.json` 목록을 TUI `OptionList`로, 고르면 그 본문으로 실행 | +| R6 | 가짜 서버 첫 응답만 2초 → 1번 줄 note `first call`, "p50 excl. first" 따로. `--warmup 1` → 시간 목록 n개(warmup 제외) | +| R7 | 없는 포트 → `connect` 1, 지연 3초 + `--timeout 1` → `timeout` 1, 500 → `http_5xx` 1, 404 → `http_4xx` 1 | +| R8 | `--header "Authorization: Bearer sk-test"` → 저장 · 기록 파일 `grep -c sk-test` 0, 대신 `"Authorization": "***"` | +| R9 | 응답 JSON의 `timings`(숫자 칸만)를 칸마다 평균. 내 헬프데스크 → `classify_ms … total_ms` 평균 한 줄. `timings`가 없으면 표 없음 | +| R10 | `--max-p95 2.5 --max-failures 0`: 넘으면 마지막 줄 `FAIL p95 3.1 > 2.5`, 종료 코드 1. 아니면 `PASS`, 0 | +| R11 | `--log runs.jsonl`: 줄마다 `{"t", "i", "status", "seconds", "error_kind"}`, 줄 수 = N(+ warmup은 `"warmup": true`) | +| R12 | `~/.tbench/profiles.toml`, `tbench profile save/list`, `tbench run <이름>`: 다른 폴더에서도 같은 결과 파일 조건 | +| R13 | 기본 저장 폴더 `~/.tbench/results/`, `tbench history` → 최신이 위, 날짜 · URL · N · p50 · p95 · 실패 | +| R14 | `uv tool install --editable .` 뒤 `cd /tmp && tbench --help` 성공, `uv tool list`에 `tbench` | + +숨은 요구 · 범위 밖은 VIBE-Q 표와 같습니다. + +## 3. `Doc/IMP-PLAN.md` 예 + +```markdown +# tbench IMP-PLAN + +## 엔진 1단계 · 부르고 재기 (R1 · R6 · R7 · R8) ← 7교시 +- [ ] src/tbench/runner.py: call_once(url, method, body, headers, timeout) → {i, status, seconds, error_kind} +- [ ] src/tbench/stats.py: percentile(nearest-rank), summarize(results) → p50 · p95 · max · failures · by_kind · p50_excl_first +- [ ] tests/conftest.py: 가짜 서버 (지연 · N번째마다 500 · 첫 응답만 느림 · 404) +- [ ] cli.py: tbench --body-file --n --method --timeout --header --warmup --plain +- 확인: uv run pytest -q · uv run tbench -n 5 --plain + +## TUI 1단계 · 오는 대로 보이기 (R2) ← 7교시 +- [ ] app.py: Sparkline · 요약 Static · DataTable, worker 가 runner 를 돌리며 결과마다 call_from_thread 로 갱신 +- 확인: uv run tbench -n 20 (가짜 서버 지연 0.5초) + +## 설치 (R14) ← 7교시 끝 +- [ ] pyproject [project.scripts] tbench = "tbench.cli:main", uv tool install --editable . + +## 엔진 2단계 · 남기고 비교 (R3 · R13 · R11) +- [ ] save(result, path=None) 기본 ~/.tbench/results/<시각>.json, compare(a, b), history() +- [ ] --log runs.jsonl +## 엔진 3단계 · 동시 · 기준 · 응답 분석 · 설정 (R4 · R10 · R9 · R12) +## TUI 2단계 · 고르기 · 비교 · 기록 화면 (R5 · R3 · R13) +``` + +## 4. 단계별 요청문 예 + +### 엔진 1단계 · 부르고 재기 + +```text +Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 "엔진 1단계"만 만들어 줘. uv 프로젝트(src/tbench), 명령 이름 tbench, 코드와 화면 글자는 영어. +tbench [--body-file f.json] [-n 10] [--method POST] [--timeout 30] [--header "K: V" ...] [--warmup 0] --plain +- 한 번마다 "#i status seconds note", 끝에 p50 · p95 · max · failures(종류별: connect · timeout · http_5xx · http_4xx), p50 excl. first. + 시간은 보내기 직전 ~ 본문을 다 받을 때까지. 실패도 한 번으로 셈. 1번 줄 note 는 "first call". warmup 은 재지 않고 버림. +- 백분위: 정렬 후 nearest-rank (ceil(p*n) 번째). stats.py 로 분리하고 pytest 로 1..10 → p50 5, p95 10. +- 엔진은 결과를 하나씩 돌려주는 제너레이터로 (나중에 TUI가 받음). 화면 코드는 아직 쓰지 않는다. +- 테스트는 pytest 안에서 http.server 로 가짜 서버(지연, N번째마다 500, 첫 응답만 느림, 404)를 띄워서. 진짜 n8n 을 부르지 않는다. +- Authorization 등 키 머리글은 출력 · 저장에서 "***". +끝나면 IMP-PLAN 의 엔진 1단계 체크를 표시하고 확인 명령을 알려 줘. +``` + +### TUI 1단계 · 오는 대로 보이기 + +```text +IMP-PLAN 의 "TUI 1단계"만 해 줘. tbench ... (--plain 없이) 는 Textual 앱: +위 Sparkline(시간 목록), 요약 줄(완료/전체, 실패, p50, p95, max, p50 excl. first), 아래 DataTable(#, status, seconds, note). +엔진 제너레이터를 worker(thread)에서 돌리고 결과마다 call_from_thread 로 표 · 스파크라인 · 요약을 갱신. 엔진 코드는 바꾸지 않는다. +키: q 끝. 확인: 가짜 서버 지연 0.5초 × 6 에서 한 줄씩 늘어나는지. +``` + +### 설치 + +```text +pyproject.toml 에 [project.scripts] tbench = "tbench.cli:main" 이 있는지 보고, 없으면 넣어 줘. +그리고 내가 실행할 명령만 알려 줘: uv tool install --editable . , uv tool list, cd /tmp && tbench --help +``` + +### 엔진 2단계 · 남기고 비교 + +```text +IMP-PLAN 의 "엔진 2단계"만: --save [path] (없으면 ~/.tbench/results/.json) 에 +{url, method, body_sha256, n, concurrency, warmup, started, seconds, statuses, errors} 저장 (키 머리글은 "***"). +tbench compare a.json b.json: p50 · p95 · max · failures 와 변화(%) 표, url · body_sha256 · n 이 다르면 WARNING 줄. +tbench history: ~/.tbench/results 의 파일을 최신부터 날짜 · url · n · p50 · p95 · failures 표. --log runs.jsonl: 한 번마다 한 줄. +테스트: 다른 본문 두 결과 → WARNING, 저장 파일에 sk- 없음. +``` + +### 엔진 3단계 · 동시 · 기준 · 응답 분석 · 설정 + +```text +IMP-PLAN 의 "엔진 3단계"만: --concurrency k (스레드 풀, 결과는 끝난 순서로 내보내되 번호 유지), +--max-p95 초 · --max-failures 수 → 마지막 줄 PASS/FAIL, 종료 코드 0/1, +응답 JSON 에 timings 가 있으면 숫자 칸마다 평균 (요약 아래 한 줄), +~/.tbench/profiles.toml: tbench profile save [옵션], tbench profile list, tbench run [-n ...]. +가짜 서버 테스트: 지연 0.3초 × 6 에서 k=3 이 k=1 보다 빠름, 기준을 넘으면 종료 코드 1. +``` + +## 5. 7교시에 볼 것 + +| 볼 것 | 막혔을 때 한마디 | +| --- | --- | +| TODO · SPEC 없이 에이전트부터 | "요청서로 Doc/TODO.md부터. 확인 방법이 없으면 됐는지 모릅니다" | +| IMP-PLAN이 한 단계짜리 | "엔진과 화면을 나누고, 엔진도 1 · 2 · 3으로. 단계마다 commit" | +| 첫 요청에 14개를 다 넣음 | "IMP-PLAN의 엔진 1단계만" | +| 다 끝난 뒤에야 화면이 나옴 | "worker로 부르고 결과마다 표에 더하게" | +| p95가 max와 늘 같음 | "n이 작으면 그럴 수 있어요. 계산 방법을 테스트로 고정" | +| 테스트가 진짜 n8n을 부름 | "pytest 안에서 가짜 서버" | +| 비교가 다른 요청끼리 | "저장할 때 url · 본문 해시를 같이" | +| `tbench`를 못 찾음 | `uv tool update-shell` 뒤 새 터미널, `uv tool list` | +| 설치한 `tbench`가 예전 코드 | `--editable`로 다시, 또는 `uv tool install --force .` | + +### 자주 빠뜨리는 것 (수업 준비 때 예상) + +| 빠뜨린 것 | 생기는 일 | +| --- | --- | +| 백분위 계산 방법 | 사람마다 · 실행마다 p95가 달라 비교가 흔들림 | +| 실패도 한 번으로 셈 · 실패 종류 | 연결이 안 되면 프로그램이 멈추거나, 실패가 시간 목록에서 빠져 p95가 좋아 보임 | +| 저장 파일에 조건(url · 본문 해시 · N) | 다른 질문으로 잰 것끼리 비교 (요청서의 "망신") | +| 엔진과 화면 나누기 | `--plain` · `compare` · 종료 코드를 화면 없이 못 씀, 테스트가 어려움 | +| 가짜 서버 | 테스트할 때마다 진짜 헬프데스크 · OpenAI 호출 | +| `[project.scripts]` | `uv tool install`은 되는데 `tbench` 명령이 없음 | +| 저장 · 설정 위치를 집 폴더에 | 다른 폴더에서 실행하면 history · profile이 비어 보임 | + +## 6. 강사 바이브 코딩 (8교시, 15분 · 시간 기록) + +| 할 것 | 걸린 시간 | 메모 | +| --- | --- | --- | +| 엔진 1단계 + TUI 1단계 + 설치를 위 요청문으로 | (8교시에 잼) | | +| R10 `--max-p95` 종료 코드 (74일차 배포 전 검사의 씨앗) | (8교시에 잼) | | + +## 7. 수업 준비 때 직접 만들어 본 본 (10/26, `tbench/`) + +| 확인 | 결과 | +| --- | --- | +| `uv run pytest -q` (가짜 서버) | **12 passed** (18.8초): 백분위 · 실패 2/6 · 첫 호출 · warmup · 실패 종류 4가지 · 키 가리기 · compare WARNING · history · log · 동시 · 종료 코드 · timings · profile · TUI 한 줄씩 · `--pick` | +| `uv tool install --editable .` → `cd /tmp && tbench --help` | 됨, `uv tool list`에 `tbench v0.1.0` | +| 진짜 헬프데스크 `my-helpdesk` + `requests/faq-vpn-nocache.json` `-n 5 --plain --save --max-p95 4` | 3.947 · 1.579 · 1.532 · 1.824 · 1.429초, 1번 줄 `first call`. **p50 1.579 · p95 3.947 · max 3.947 · p50 excl. first 1.532**, `PASS` | +| 같은 때 `flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5` | p50 1.38 · p95 1.83 · max 1.83초 (바로 앞 tbench가 데워 둠) | + +(강사 Mac의 n8n에 올라가 있던 `my-helpdesk`는 규칙 분류 · 코드 검사가 들어간 판이라 `llm_calls` 1, `timings`에 `classify_ms`가 없음.) + +만들며 알게 된 것 (학생이 막힐 만한 곳): + +| 알게 된 것 | 정한 방법 | +| --- | --- | +| n이 작으면 p95 = max (위 실측: 5번이면 p95는 5번째 = 첫 호출 3.9초) | nearest-rank 그대로 두고, 첫 호출 제외 p50과 `--warmup`으로 따로 봄 (⑥) | +| `profile save`에 키를 넣으면 "남기는 파일에 키가 찍히면 안 돼요"와 부딪힘 | 키 머리글은 `$MY_KEY`처럼 환경 변수로만 저장, 글자 키는 `***`로 바꾸고 안내 (숨은 요구) | +| 응답마다 `timings` 칸이 다름 (규칙 길은 `classify_ms` 없음) | 칸마다 그 칸이 있는 응답끼리 평균 | +| 동시(`--concurrency`)면 결과가 끝난 순서로 옴 | 번호(`i`)는 보낸 순서로 유지, 저장은 번호순 | +| 본문 해시를 JSON을 다시 쓴 글자로 내면 같은 파일도 해시가 달라질 수 있음 | 파일 글자 그대로 보내고 그 글자로 해시 | +| TUI 테스트 | Textual `run_test()` + 가짜 서버 지연 0.5초: 1.3초 뒤 1~5줄, 끝나면 6줄 | + diff --git a/Last3Mon/72/answers/00-lab01-baseline.txt b/Last3Mon/72/answers/00-lab01-baseline.txt new file mode 100644 index 0000000..20e796f --- /dev/null +++ b/Last3Mon/72/answers/00-lab01-baseline.txt @@ -0,0 +1,47 @@ +$ uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v +HTTP 200 · 4.4s + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ] + } + +$ uv run tools/flow.py bench base-helpdesk requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 2.98s · route=자동 답변 +✔ #2 · HTTP 200 · 2.52s · route=자동 답변 +✔ #3 · HTTP 200 · 2.69s · route=자동 답변 +✔ #4 · HTTP 200 · 2.84s · route=자동 답변 +✔ #5 · HTTP 200 · 2.54s · route=자동 답변 +p50 2.69s · p95 2.98s · max 2.98s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174234.json + +$ uv run tools/flow.py bench base-helpdesk requests/ticket-own.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 3.04s · route=자동 답변 +✔ #2 · HTTP 200 · 3.14s · route=자동 답변 +✔ #3 · HTTP 200 · 2.61s · route=자동 답변 +✔ #4 · HTTP 200 · 3.36s · route=자동 답변 +✔ #5 · HTTP 200 · 2.68s · route=자동 답변 +p50 3.04s · p95 3.36s · max 3.36s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174249.json + +$ uv run tools/flow.py bench base-helpdesk requests/off-topic.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 2.90s · route=자동 답변 +✔ #2 · HTTP 200 · 2.55s · route=자동 답변 +✔ #3 · HTTP 200 · 2.62s · route=자동 답변 +✔ #4 · HTTP 200 · 2.56s · route=자동 답변 +✔ #5 · HTTP 200 · 2.44s · route=자동 답변 +p50 2.56s · p95 2.90s · max 2.90s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174302.json + diff --git a/Last3Mon/72/answers/01-lab01-baseline.json b/Last3Mon/72/answers/01-lab01-baseline.json new file mode 100644 index 0000000..a60801f --- /dev/null +++ b/Last3Mon/72/answers/01-lab01-baseline.json @@ -0,0 +1,95 @@ +{ + "cases": "lab01.json", + "webhook": "base-helpdesk", + "passed": 1, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "base-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "timings has none of ['answer_ms']" + ], + "seconds": 3.5, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "base-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "timings has none of ['total_ms']", + "steps has none of ['answer']", + "llm_calls has none of ['1', '2', '3']" + ], + "seconds": 2.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } + }, + { + "case": "접수 현황", + "webhook": "base-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "timings has none of ['answer_ms']" + ], + "seconds": 2.8, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [] + } + }, + { + "case": "빈 질문", + "webhook": "base-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/72/answers/01-lab01-baseline.txt b/Last3Mon/72/answers/01-lab01-baseline.txt new file mode 100644 index 0000000..d9d4d5f --- /dev/null +++ b/Last3Mon/72/answers/01-lab01-baseline.txt @@ -0,0 +1,58 @@ +POST http://localhost:5678/webhook/base-helpdesk · 4 case(s) × 1 +✘ VPN FAQ · 단계별 시간 · 3.5s · timings has none of ['answer_ms'] + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } +✘ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.9s · timings has none of ['total_ms']; steps has none of ['answer']; llm_calls has none of ['1', '2', '3'] + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } +✘ 접수 현황 · 2.8s · timings has none of ['answer_ms'] + → request POST /webhook/base-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [] + } +✔ 빈 질문 · 0.0s + → request POST /webhook/base-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/4 +report: runs/lab01-20261024-174311.json diff --git a/Last3Mon/72/answers/02-lab01.json b/Last3Mon/72/answers/02-lab01.json new file mode 100644 index 0000000..6b2d2a8 --- /dev/null +++ b/Last3Mon/72/answers/02-lab01.json @@ -0,0 +1,137 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1210, + "data_ms": 29, + "answer_ms": 1119, + "review_ms": 1042, + "total_ms": 3426 + } + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.8, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 753, + "data_ms": 19, + "answer_ms": 1058, + "review_ms": 890, + "total_ms": 2731 + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['처리 중']" + ], + "seconds": 3.3, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 929, + "data_ms": 21, + "answer_ms": 1118, + "review_ms": 1029, + "total_ms": 3108 + } + } + }, + { + "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/72/answers/02-lab01.txt b/Last3Mon/72/answers/02-lab01.txt new file mode 100644 index 0000000..3f56e7c --- /dev/null +++ b/Last3Mon/72/answers/02-lab01.txt @@ -0,0 +1,106 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 4.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1210, + "data_ms": 29, + "answer_ms": 1119, + "review_ms": 1042, + "total_ms": 3426 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 753, + "data_ms": 19, + "answer_ms": 1058, + "review_ms": 890, + "total_ms": 2731 + } + } +✘ 접수 현황 · 3.3s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 929, + "data_ms": 21, + "answer_ms": 1118, + "review_ms": 1029, + "total_ms": 3108 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/4 +report: runs/lab01-20261024-174333.json diff --git a/Last3Mon/72/answers/03-lab01-call.txt b/Last3Mon/72/answers/03-lab01-call.txt new file mode 100644 index 0000000..cb6c3b8 --- /dev/null +++ b/Last3Mon/72/answers/03-lab01-call.txt @@ -0,0 +1,33 @@ +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +HTTP 200 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하시고, 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 616, + "data_ms": 18, + "answer_ms": 1124, + "review_ms": 920, + "total_ms": 2698 + } + } + diff --git a/Last3Mon/72/answers/04-lab01-kinds.txt b/Last3Mon/72/answers/04-lab01-kinds.txt new file mode 100644 index 0000000..0b2321d --- /dev/null +++ b/Last3Mon/72/answers/04-lab01-kinds.txt @@ -0,0 +1,92 @@ +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.57s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 3.08s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 2.91s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.80s · route=자동 답변, llm_calls=3, cached=false +p50 2.91s · p95 3.08s · max 3.08s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174351.json + +$ uv run tools/flow.py bench my-helpdesk requests/ticket-own.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.86s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 3.05s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 3.12s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.94s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false +p50 2.99s · p95 3.12s · max 3.12s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174406.json + +$ uv run tools/flow.py bench my-helpdesk requests/off-topic.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.38s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 2.36s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 2.48s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.49s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.84s · route=자동 답변, llm_calls=3, cached=false +p50 2.48s · p95 2.84s · max 2.84s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174419.json + +$ uv run tools/flow.py call my-helpdesk requests/ticket-own.json -v +HTTP 200 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 818, + "data_ms": 43, + "answer_ms": 877, + "review_ms": 895, + "total_ms": 2647 + } + } + +$ uv run tools/flow.py call my-helpdesk requests/off-topic.json -v +HTTP 200 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "other", + "answer": "찾으시는 정보를 확인하지 못했습니다. 점심 메뉴 추천은 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 732, + "data_ms": 19, + "answer_ms": 740, + "review_ms": 978, + "total_ms": 2482 + } + } + diff --git a/Last3Mon/72/answers/05-lab01-challenge-before.json b/Last3Mon/72/answers/05-lab01-challenge-before.json new file mode 100644 index 0000000..8fd49d9 --- /dev/null +++ b/Last3Mon/72/answers/05-lab01-challenge-before.json @@ -0,0 +1,166 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 0, + "total": 4, + "results": [ + { + "case": "FAQ · 접수 시간 따로", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "timings has none of ['faq_ms']" + ], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 595, + "data_ms": 18, + "answer_ms": 1005, + "review_ms": 1012, + "total_ms": 2643 + } + } + }, + { + "case": "접수 조회 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "timings has none of ['ticket_ms']" + ], + "seconds": 2.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 716, + "data_ms": 19, + "answer_ms": 958, + "review_ms": 881, + "total_ms": 2594 + } + } + }, + { + "case": "가장 오래 걸린 단계", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "slowest_step has none of ['classify', 'answer', 'review']" + ], + "seconds": 3.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 991, + "data_ms": 18, + "answer_ms": 1024, + "review_ms": 991, + "total_ms": 3037 + } + } + }, + { + "case": "접수 현황도 같은 모양", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['처리 중']", + "slowest_step has none of ['classify', 'answer', 'review']" + ], + "seconds": 2.8, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 803, + "data_ms": 36, + "answer_ms": 884, + "review_ms": 1043, + "total_ms": 2781 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/05-lab01-challenge-before.txt b/Last3Mon/72/answers/05-lab01-challenge-before.txt new file mode 100644 index 0000000..b62498b --- /dev/null +++ b/Last3Mon/72/answers/05-lab01-challenge-before.txt @@ -0,0 +1,128 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✘ FAQ · 접수 시간 따로 · 2.7s · timings has none of ['faq_ms'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 595, + "data_ms": 18, + "answer_ms": 1005, + "review_ms": 1012, + "total_ms": 2643 + } + } +✘ 접수 조회 시간 · 2.6s · timings has none of ['ticket_ms'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 716, + "data_ms": 19, + "answer_ms": 958, + "review_ms": 881, + "total_ms": 2594 + } + } +✘ 가장 오래 걸린 단계 · 3.1s · slowest_step has none of ['classify', 'answer', 'review'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 991, + "data_ms": 18, + "answer_ms": 1024, + "review_ms": 991, + "total_ms": 3037 + } + } +✘ 접수 현황도 같은 모양 · 2.8s · answer has none of ['처리 중']; slowest_step has none of ['classify', 'answer', 'review'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 803, + "data_ms": 36, + "answer_ms": 884, + "review_ms": 1043, + "total_ms": 2781 + } + } +Passed 0/4 +report: runs/lab01-challenge-20261024-174435.json diff --git a/Last3Mon/72/answers/06-lab01-challenge.json b/Last3Mon/72/answers/06-lab01-challenge.json new file mode 100644 index 0000000..6541312 --- /dev/null +++ b/Last3Mon/72/answers/06-lab01-challenge.json @@ -0,0 +1,165 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "FAQ · 접수 시간 따로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.5, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1365, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1196, + "review_ms": 993, + "total_ms": 3614 + }, + "slowest_step": "classify" + } + }, + { + "case": "접수 조회 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 774, + "faq_ms": 17, + "ticket_ms": 13, + "answer_ms": 971, + "review_ms": 815, + "total_ms": 2601 + }, + "slowest_step": "answer" + } + }, + { + "case": "가장 오래 걸린 단계", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 680, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1044, + "review_ms": 1007, + "total_ms": 2769 + }, + "slowest_step": "answer" + } + }, + { + "case": "접수 현황도 같은 모양", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 723, + "faq_ms": 13, + "ticket_ms": 11, + "answer_ms": 1029, + "review_ms": 1162, + "total_ms": 2951 + }, + "slowest_step": "review" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/06-lab01-challenge.txt b/Last3Mon/72/answers/06-lab01-challenge.txt new file mode 100644 index 0000000..ab22ad1 --- /dev/null +++ b/Last3Mon/72/answers/06-lab01-challenge.txt @@ -0,0 +1,136 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ FAQ · 접수 시간 따로 · 4.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1365, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1196, + "review_ms": 993, + "total_ms": 3614 + }, + "slowest_step": "classify" + } +✔ 접수 조회 시간 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 774, + "faq_ms": 17, + "ticket_ms": 13, + "answer_ms": 971, + "review_ms": 815, + "total_ms": 2601 + }, + "slowest_step": "answer" + } +✔ 가장 오래 걸린 단계 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 680, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1044, + "review_ms": 1007, + "total_ms": 2769 + }, + "slowest_step": "answer" + } +✔ 접수 현황도 같은 모양 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 723, + "faq_ms": 13, + "ticket_ms": 11, + "answer_ms": 1029, + "review_ms": 1162, + "total_ms": 2951 + }, + "slowest_step": "review" + } +Passed 4/4 +report: runs/lab01-challenge-20261024-174502.json diff --git a/Last3Mon/72/answers/07-lab01-after-challenge.json b/Last3Mon/72/answers/07-lab01-after-challenge.json new file mode 100644 index 0000000..40627f5 --- /dev/null +++ b/Last3Mon/72/answers/07-lab01-after-challenge.json @@ -0,0 +1,141 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 844, + "faq_ms": 16, + "ticket_ms": 9, + "answer_ms": 990, + "review_ms": 927, + "total_ms": 2807 + }, + "slowest_step": "answer" + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.8, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 715, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1064, + "review_ms": 988, + "total_ms": 2810 + }, + "slowest_step": "answer" + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 810, + "faq_ms": 16, + "ticket_ms": 11, + "answer_ms": 1014, + "review_ms": 1058, + "total_ms": 2925 + }, + "slowest_step": "review" + } + }, + { + "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/72/answers/07-lab01-after-challenge.txt b/Last3Mon/72/answers/07-lab01-after-challenge.txt new file mode 100644 index 0000000..a8eedba --- /dev/null +++ b/Last3Mon/72/answers/07-lab01-after-challenge.txt @@ -0,0 +1,112 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 844, + "faq_ms": 16, + "ticket_ms": 9, + "answer_ms": 990, + "review_ms": 927, + "total_ms": 2807 + }, + "slowest_step": "answer" + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 715, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1064, + "review_ms": 988, + "total_ms": 2810 + }, + "slowest_step": "answer" + } +✔ 접수 현황 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 810, + "faq_ms": 16, + "ticket_ms": 11, + "answer_ms": 1014, + "review_ms": 1058, + "total_ms": 2925 + }, + "slowest_step": "review" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174511.json diff --git a/Last3Mon/72/answers/08-lab01-challenge2-before.json b/Last3Mon/72/answers/08-lab01-challenge2-before.json new file mode 100644 index 0000000..60ef5e2 --- /dev/null +++ b/Last3Mon/72/answers/08-lab01-challenge2-before.json @@ -0,0 +1,180 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 0, + "total": 4, + "results": [ + { + "case": "예산 안 (넉넉한 예산)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "over_budget = null (expected false)", + "budget_ms = null (expected 60000)", + "llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']" + ], + "seconds": 2.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 628, + "faq_ms": 11, + "ticket_ms": 11, + "answer_ms": 1142, + "review_ms": 1028, + "total_ms": 2862 + }, + "slowest_step": "answer" + } + }, + { + "case": "예산 넘음 (0.5초 예산)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "over_budget = null (expected true)", + "budget_ms = null (expected 500)" + ], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 658, + "faq_ms": 14, + "ticket_ms": 11, + "answer_ms": 1013, + "review_ms": 930, + "total_ms": 2640 + }, + "slowest_step": "answer" + } + }, + { + "case": "예산을 안 주면 3초", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "budget_ms = null (expected 3000)", + "llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']" + ], + "seconds": 3.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 650, + "faq_ms": 14, + "ticket_ms": 13, + "answer_ms": 1767, + "review_ms": 1061, + "total_ms": 3520 + }, + "slowest_step": "answer" + } + }, + { + "case": "접수 현황도 같은 모양", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "budget_ms = null (expected 3000)", + "llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']" + ], + "seconds": 2.8, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 772, + "faq_ms": 14, + "ticket_ms": 12, + "answer_ms": 895, + "review_ms": 1006, + "total_ms": 2719 + }, + "slowest_step": "review" + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/08-lab01-challenge2-before.txt b/Last3Mon/72/answers/08-lab01-challenge2-before.txt new file mode 100644 index 0000000..31a4414 --- /dev/null +++ b/Last3Mon/72/answers/08-lab01-challenge2-before.txt @@ -0,0 +1,138 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✘ 예산 안 (넉넉한 예산) · 2.9s · over_budget = null (expected false); budget_ms = null (expected 60000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 628, + "faq_ms": 11, + "ticket_ms": 11, + "answer_ms": 1142, + "review_ms": 1028, + "total_ms": 2862 + }, + "slowest_step": "answer" + } +✘ 예산 넘음 (0.5초 예산) · 2.7s · over_budget = null (expected true); budget_ms = null (expected 500) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 658, + "faq_ms": 14, + "ticket_ms": 11, + "answer_ms": 1013, + "review_ms": 930, + "total_ms": 2640 + }, + "slowest_step": "answer" + } +✘ 예산을 안 주면 3초 · 3.6s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 650, + "faq_ms": 14, + "ticket_ms": 13, + "answer_ms": 1767, + "review_ms": 1061, + "total_ms": 3520 + }, + "slowest_step": "answer" + } +✘ 접수 현황도 같은 모양 · 2.8s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 772, + "faq_ms": 14, + "ticket_ms": 12, + "answer_ms": 895, + "review_ms": 1006, + "total_ms": 2719 + }, + "slowest_step": "review" + } +Passed 0/4 +report: runs/lab01-challenge2-20261024-174523.json diff --git a/Last3Mon/72/answers/09-lab01-challenge2.json b/Last3Mon/72/answers/09-lab01-challenge2.json new file mode 100644 index 0000000..cadc022 --- /dev/null +++ b/Last3Mon/72/answers/09-lab01-challenge2.json @@ -0,0 +1,179 @@ +{ + "cases": "lab01-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "예산 안 (넉넉한 예산)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1208, + "faq_ms": 17, + "ticket_ms": 14, + "answer_ms": 1230, + "review_ms": 857, + "total_ms": 3357 + }, + "slowest_step": "answer", + "llm_ms": 3295, + "budget_ms": 60000, + "over_budget": false + } + }, + { + "case": "예산 넘음 (0.5초 예산)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 667, + "faq_ms": 15, + "ticket_ms": 11, + "answer_ms": 980, + "review_ms": 916, + "total_ms": 2609 + }, + "slowest_step": "answer", + "llm_ms": 2563, + "budget_ms": 500, + "over_budget": true + } + }, + { + "case": "예산을 안 주면 3초", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 761, + "faq_ms": 18, + "ticket_ms": 18, + "answer_ms": 1027, + "review_ms": 1185, + "total_ms": 3023 + }, + "slowest_step": "review", + "llm_ms": 2973, + "budget_ms": 3000, + "over_budget": true + } + }, + { + "case": "접수 현황도 같은 모양", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 795, + "faq_ms": 13, + "ticket_ms": 14, + "answer_ms": 1017, + "review_ms": 1046, + "total_ms": 2904 + }, + "slowest_step": "review", + "llm_ms": 2858, + "budget_ms": 3000, + "over_budget": false + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/09-lab01-challenge2.txt b/Last3Mon/72/answers/09-lab01-challenge2.txt new file mode 100644 index 0000000..f0ca447 --- /dev/null +++ b/Last3Mon/72/answers/09-lab01-challenge2.txt @@ -0,0 +1,150 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 예산 안 (넉넉한 예산) · 4.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1208, + "faq_ms": 17, + "ticket_ms": 14, + "answer_ms": 1230, + "review_ms": 857, + "total_ms": 3357 + }, + "slowest_step": "answer", + "llm_ms": 3295, + "budget_ms": 60000, + "over_budget": false + } +✔ 예산 넘음 (0.5초 예산) · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 667, + "faq_ms": 15, + "ticket_ms": 11, + "answer_ms": 980, + "review_ms": 916, + "total_ms": 2609 + }, + "slowest_step": "answer", + "llm_ms": 2563, + "budget_ms": 500, + "over_budget": true + } +✔ 예산을 안 주면 3초 · 3.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 761, + "faq_ms": 18, + "ticket_ms": 18, + "answer_ms": 1027, + "review_ms": 1185, + "total_ms": 3023 + }, + "slowest_step": "review", + "llm_ms": 2973, + "budget_ms": 3000, + "over_budget": true + } +✔ 접수 현황도 같은 모양 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 795, + "faq_ms": 13, + "ticket_ms": 14, + "answer_ms": 1017, + "review_ms": 1046, + "total_ms": 2904 + }, + "slowest_step": "review", + "llm_ms": 2858, + "budget_ms": 3000, + "over_budget": false + } +Passed 4/4 +report: runs/lab01-challenge2-20261024-174548.json diff --git a/Last3Mon/72/answers/10-lab01-challenge-after2.json b/Last3Mon/72/answers/10-lab01-challenge-after2.json new file mode 100644 index 0000000..2bddb41 --- /dev/null +++ b/Last3Mon/72/answers/10-lab01-challenge-after2.json @@ -0,0 +1,177 @@ +{ + "cases": "lab01-challenge.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "FAQ · 접수 시간 따로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 603, + "faq_ms": 9, + "ticket_ms": 10, + "answer_ms": 969, + "review_ms": 1043, + "total_ms": 2677 + }, + "slowest_step": "review", + "llm_ms": 2615, + "budget_ms": 3000, + "over_budget": false + } + }, + { + "case": "접수 조회 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 614, + "faq_ms": 13, + "ticket_ms": 13, + "answer_ms": 1019, + "review_ms": 876, + "total_ms": 2543 + }, + "slowest_step": "answer", + "llm_ms": 2509, + "budget_ms": 3000, + "over_budget": false + } + }, + { + "case": "가장 오래 걸린 단계", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 687, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1146, + "review_ms": 965, + "total_ms": 2838 + }, + "slowest_step": "answer", + "llm_ms": 2798, + "budget_ms": 3000, + "over_budget": false + } + }, + { + "case": "접수 현황도 같은 모양", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 767, + "faq_ms": 15, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1007, + "total_ms": 2865 + }, + "slowest_step": "answer", + "llm_ms": 2827, + "budget_ms": 3000, + "over_budget": false + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/10-lab01-challenge-after2.txt b/Last3Mon/72/answers/10-lab01-challenge-after2.txt new file mode 100644 index 0000000..177a95c --- /dev/null +++ b/Last3Mon/72/answers/10-lab01-challenge-after2.txt @@ -0,0 +1,148 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ FAQ · 접수 시간 따로 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 603, + "faq_ms": 9, + "ticket_ms": 10, + "answer_ms": 969, + "review_ms": 1043, + "total_ms": 2677 + }, + "slowest_step": "review", + "llm_ms": 2615, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 조회 시간 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 614, + "faq_ms": 13, + "ticket_ms": 13, + "answer_ms": 1019, + "review_ms": 876, + "total_ms": 2543 + }, + "slowest_step": "answer", + "llm_ms": 2509, + "budget_ms": 3000, + "over_budget": false + } +✔ 가장 오래 걸린 단계 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 687, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1146, + "review_ms": 965, + "total_ms": 2838 + }, + "slowest_step": "answer", + "llm_ms": 2798, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 현황도 같은 모양 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 767, + "faq_ms": 15, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1007, + "total_ms": 2865 + }, + "slowest_step": "answer", + "llm_ms": 2827, + "budget_ms": 3000, + "over_budget": false + } +Passed 4/4 +report: runs/lab01-challenge-20261024-174559.json diff --git a/Last3Mon/72/answers/11-lab01-after2.json b/Last3Mon/72/answers/11-lab01-after2.json new file mode 100644 index 0000000..02d8a13 --- /dev/null +++ b/Last3Mon/72/answers/11-lab01-after2.json @@ -0,0 +1,150 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 785, + "faq_ms": 14, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1031, + "total_ms": 2902 + }, + "slowest_step": "answer", + "llm_ms": 2869, + "budget_ms": 3000, + "over_budget": false + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 750, + "faq_ms": 15, + "ticket_ms": 14, + "answer_ms": 984, + "review_ms": 895, + "total_ms": 2672 + }, + "slowest_step": "answer", + "llm_ms": 2629, + "budget_ms": 3000, + "over_budget": false + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.4, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 938, + "faq_ms": 10, + "ticket_ms": 9, + "answer_ms": 981, + "review_ms": 1355, + "total_ms": 3310 + }, + "slowest_step": "review", + "llm_ms": 3274, + "budget_ms": 3000, + "over_budget": true + } + }, + { + "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/72/answers/11-lab01-after2.txt b/Last3Mon/72/answers/11-lab01-after2.txt new file mode 100644 index 0000000..9baf08a --- /dev/null +++ b/Last3Mon/72/answers/11-lab01-after2.txt @@ -0,0 +1,121 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 785, + "faq_ms": 14, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1031, + "total_ms": 2902 + }, + "slowest_step": "answer", + "llm_ms": 2869, + "budget_ms": 3000, + "over_budget": false + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 750, + "faq_ms": 15, + "ticket_ms": 14, + "answer_ms": 984, + "review_ms": 895, + "total_ms": 2672 + }, + "slowest_step": "answer", + "llm_ms": 2629, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 현황 · 3.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 938, + "faq_ms": 10, + "ticket_ms": 9, + "answer_ms": 981, + "review_ms": 1355, + "total_ms": 3310 + }, + "slowest_step": "review", + "llm_ms": 3274, + "budget_ms": 3000, + "over_budget": true + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174609.json diff --git a/Last3Mon/72/answers/12-lab02-rules.json b/Last3Mon/72/answers/12-lab02-rules.json new file mode 100644 index 0000000..2a62277 --- /dev/null +++ b/Last3Mon/72/answers/12-lab02-rules.json @@ -0,0 +1,332 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 7, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.8, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 41, + "answer_ms": 1766, + "review_ms": 1115, + "total_ms": 2945 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.5, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1041, + "review_ms": 1209, + "total_ms": 2295 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.1, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 23, + "answer_ms": 1209, + "review_ms": 797, + "total_ms": 2040 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.4, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 21, + "classify_ms": 582, + "answer_ms": 908, + "review_ms": 877, + "total_ms": 2401 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 1202, + "review_ms": 994, + "total_ms": 2234 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "took 1.9s (expected at most 1s)", + "cached = false (expected true)", + "llm_calls = 2 (expected 0)" + ], + "seconds": 1.9, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 문제가 계속되면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 24, + "answer_ms": 967, + "review_ms": 890, + "total_ms": 1898 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 17, + "answer_ms": 1128, + "review_ms": 1048, + "total_ms": 2213 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 829, + "review_ms": 1300, + "total_ms": 2171 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['처리 중']" + ], + "seconds": 2.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 951, + "review_ms": 940, + "total_ms": 1924 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/12-lab02-rules.txt b/Last3Mon/72/answers/12-lab02-rules.txt new file mode 100644 index 0000000..00d5122 --- /dev/null +++ b/Last3Mon/72/answers/12-lab02-rules.txt @@ -0,0 +1,268 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 41, + "answer_ms": 1766, + "review_ms": 1115, + "total_ms": 2945 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1041, + "review_ms": 1209, + "total_ms": 2295 + } + } +✔ 예제 1 · 없는 접수 번호 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 23, + "answer_ms": 1209, + "review_ms": 797, + "total_ms": 2040 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.4s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 21, + "classify_ms": 582, + "answer_ms": 908, + "review_ms": 877, + "total_ms": 2401 + } + } +✔ 예제 2 · 처음 묻는 VPN · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 1202, + "review_ms": 994, + "total_ms": 2234 + } + } +✘ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 1.9s · took 1.9s (expected at most 1s); cached = false (expected true); llm_calls = 2 (expected 0) + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 문제가 계속되면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 24, + "answer_ms": 967, + "review_ms": 890, + "total_ms": 1898 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 17, + "answer_ms": 1128, + "review_ms": 1048, + "total_ms": 2213 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 829, + "review_ms": 1300, + "total_ms": 2171 + } + } +✘ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.0s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 951, + "review_ms": 940, + "total_ms": 1924 + } + } +Passed 7/9 +report: runs/lab02-20261024-174642.json diff --git a/Last3Mon/72/answers/13-lab02-rules-bench.txt b/Last3Mon/72/answers/13-lab02-rules-bench.txt new file mode 100644 index 0000000..9e4c067 --- /dev/null +++ b/Last3Mon/72/answers/13-lab02-rules-bench.txt @@ -0,0 +1,10 @@ +$ uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.50s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.51s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 2.39s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.12s · route=자동 답변, llm_calls=2, cached=false +p50 2.45s · p95 2.51s · max 2.51s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174654.json + diff --git a/Last3Mon/72/answers/14-lab02-cache.json b/Last3Mon/72/answers/14-lab02-cache.json new file mode 100644 index 0000000..d219a4b --- /dev/null +++ b/Last3Mon/72/answers/14-lab02-cache.json @@ -0,0 +1,319 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.9, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 76, + "answer_ms": 1837, + "review_ms": 1048, + "total_ms": 2981 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 911, + "review_ms": 1002, + "total_ms": 1957 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.0, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 40, + "answer_ms": 1008, + "review_ms": 870, + "total_ms": 1932 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.6, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "classify_ms": 767, + "answer_ms": 810, + "review_ms": 962, + "total_ms": 2586 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 58, + "answer_ms": 1140, + "review_ms": 929, + "total_ms": 2143 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 34 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1024, + "review_ms": 1125, + "total_ms": 2194 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 35, + "answer_ms": 964, + "review_ms": 1161, + "total_ms": 2173 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "answer_ms": 886, + "review_ms": 1239, + "total_ms": 2168 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/14-lab02-cache.txt b/Last3Mon/72/answers/14-lab02-cache.txt new file mode 100644 index 0000000..0b3595b --- /dev/null +++ b/Last3Mon/72/answers/14-lab02-cache.txt @@ -0,0 +1,261 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.9s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 76, + "answer_ms": 1837, + "review_ms": 1048, + "total_ms": 2981 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 911, + "review_ms": 1002, + "total_ms": 1957 + } + } +✔ 예제 1 · 없는 접수 번호 · 2.0s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 40, + "answer_ms": 1008, + "review_ms": 870, + "total_ms": 1932 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "classify_ms": 767, + "answer_ms": 810, + "review_ms": 962, + "total_ms": 2586 + } + } +✔ 예제 2 · 처음 묻는 VPN · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 58, + "answer_ms": 1140, + "review_ms": 929, + "total_ms": 2143 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 34 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1024, + "review_ms": 1125, + "total_ms": 2194 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 35, + "answer_ms": 964, + "review_ms": 1161, + "total_ms": 2173 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "answer_ms": 886, + "review_ms": 1239, + "total_ms": 2168 + } + } +Passed 9/9 +report: runs/lab02-20261024-174725.json diff --git a/Last3Mon/72/answers/15-lab01-cache.json b/Last3Mon/72/answers/15-lab01-cache.json new file mode 100644 index 0000000..ab80124 --- /dev/null +++ b/Last3Mon/72/answers/15-lab01-cache.json @@ -0,0 +1,134 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 34, + "answer_ms": 1125, + "review_ms": 852, + "total_ms": 2029 + } + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1182, + "review_ms": 890, + "total_ms": 2114 + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "answer has none of ['처리 중']" + ], + "seconds": 2.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 50, + "answer_ms": 884, + "review_ms": 1046, + "total_ms": 1992 + } + } + }, + { + "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/72/answers/15-lab01-cache.txt b/Last3Mon/72/answers/15-lab01-cache.txt new file mode 100644 index 0000000..ec39a63 --- /dev/null +++ b/Last3Mon/72/answers/15-lab01-cache.txt @@ -0,0 +1,103 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 34, + "answer_ms": 1125, + "review_ms": 852, + "total_ms": 2029 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1182, + "review_ms": 890, + "total_ms": 2114 + } + } +✘ 접수 현황 · 2.1s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 50, + "answer_ms": 884, + "review_ms": 1046, + "total_ms": 1992 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/4 +report: runs/lab01-20261024-174732.json diff --git a/Last3Mon/72/answers/16-cache-stats.txt b/Last3Mon/72/answers/16-cache-stats.txt new file mode 100644 index 0000000..82bf035 --- /dev/null +++ b/Last3Mon/72/answers/16-cache-stats.txt @@ -0,0 +1,3 @@ +$ curl -s localhost:8800/cache-stats; echo +{"entries": 2, "hits": 4, "misses": 8} + diff --git a/Last3Mon/72/answers/17-cache-hit-bench.txt b/Last3Mon/72/answers/17-cache-hit-bench.txt new file mode 100644 index 0000000..863d347 --- /dev/null +++ b/Last3Mon/72/answers/17-cache-hit-bench.txt @@ -0,0 +1,26 @@ +$ curl -s -XPOST localhost:8800/cache-clear; echo +{"cleared": 2} + +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.07s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 0.06s · route=자동 답변, llm_calls=0, cached=true +✔ #3 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true +✔ #4 · HTTP 200 · 0.04s · route=자동 답변, llm_calls=0, cached=true +✔ #5 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true +p50 0.05s · p95 2.07s · max 2.07s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174734.json + +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.99s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 1.98s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.21s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 1.97s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.24s · route=자동 답변, llm_calls=2, cached=false +p50 1.99s · p95 2.24s · max 2.24s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174745.json + +$ curl -s localhost:8800/cache-stats; echo +{"entries": 1, "hits": 9, "misses": 1} + diff --git a/Last3Mon/72/answers/18-lab02-challenge-before.json b/Last3Mon/72/answers/18-lab02-challenge-before.json new file mode 100644 index 0000000..67e655b --- /dev/null +++ b/Last3Mon/72/answers/18-lab02-challenge-before.json @@ -0,0 +1,177 @@ +{ + "cases": "lab02-challenge.json", + "webhook": "my-helpdesk", + "passed": 1, + "total": 5, + "results": [ + { + "case": "VPN · LLM 한 번 · 2.5초 안", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "llm_calls = 2 (expected 1)" + ], + "seconds": 2.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1054, + "review_ms": 1038, + "total_ms": 2141 + } + } + }, + { + "case": "비밀번호 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "llm_calls = 2 (expected 1)" + ], + "seconds": 2.0, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 72, + "answer_ms": 943, + "review_ms": 950, + "total_ms": 1984 + } + } + }, + { + "case": "접수 현황 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "llm_calls = 2 (expected 1)" + ], + "seconds": 2.3, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 26, + "answer_ms": 932, + "review_ms": 1241, + "total_ms": 2213 + } + } + }, + { + "case": "규칙으로 모르면 분류 + 답 두 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "llm_calls = 3 (expected 2)" + ], + "seconds": 2.8, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "classify_ms": 730, + "answer_ms": 1041, + "review_ms": 926, + "total_ms": 2736 + } + } + }, + { + "case": "빈 질문은 LLM 없이", + "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/72/answers/18-lab02-challenge-before.txt b/Last3Mon/72/answers/18-lab02-challenge-before.txt new file mode 100644 index 0000000..035b017 --- /dev/null +++ b/Last3Mon/72/answers/18-lab02-challenge-before.txt @@ -0,0 +1,135 @@ +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.0s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ VPN · LLM 한 번 · 2.5초 안 · 2.2s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1054, + "review_ms": 1038, + "total_ms": 2141 + } + } +✘ 비밀번호 · LLM 한 번 · 2.0s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 72, + "answer_ms": 943, + "review_ms": 950, + "total_ms": 1984 + } + } +✘ 접수 현황 · LLM 한 번 · 2.3s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 26, + "answer_ms": 932, + "review_ms": 1241, + "total_ms": 2213 + } + } +✘ 규칙으로 모르면 분류 + 답 두 번 · 2.8s · llm_calls = 3 (expected 2) + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "classify_ms": 730, + "answer_ms": 1041, + "review_ms": 926, + "total_ms": 2736 + } + } +✔ 빈 질문은 LLM 없이 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/5 +report: runs/lab02-challenge-20261024-174756.json diff --git a/Last3Mon/72/answers/19-bench-before.txt b/Last3Mon/72/answers/19-bench-before.txt new file mode 100644 index 0000000..53c6051 --- /dev/null +++ b/Last3Mon/72/answers/19-bench-before.txt @@ -0,0 +1,10 @@ +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.26s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.11s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 2.22s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.03s · route=자동 답변, llm_calls=2, cached=false +p50 2.22s · p95 2.45s · max 2.45s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174807.json + diff --git a/Last3Mon/72/answers/20-lab02-challenge.json b/Last3Mon/72/answers/20-lab02-challenge.json new file mode 100644 index 0000000..46fa866 --- /dev/null +++ b/Last3Mon/72/answers/20-lab02-challenge.json @@ -0,0 +1,491 @@ +{ + "cases": "lab02-challenge.json", + "webhook": "my-helpdesk", + "passed": 15, + "total": 15, + "results": [ + { + "case": "VPN · LLM 한 번 · 2.5초 안", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 37, + "answer_ms": 986, + "review_ms": 11, + "total_ms": 1046 + } + } + }, + { + "case": "VPN · LLM 한 번 · 2.5초 안", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1057, + "review_ms": 14, + "total_ms": 1107 + } + } + }, + { + "case": "VPN · LLM 한 번 · 2.5초 안", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 41, + "answer_ms": 952, + "review_ms": 20, + "total_ms": 1023 + } + } + }, + { + "case": "비밀번호 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 81, + "answer_ms": 868, + "review_ms": 16, + "total_ms": 974 + } + } + }, + { + "case": "비밀번호 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 824, + "review_ms": 15, + "total_ms": 881 + } + } + }, + { + "case": "비밀번호 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 48, + "answer_ms": 811, + "review_ms": 12, + "total_ms": 879 + } + } + }, + { + "case": "접수 현황 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 798, + "review_ms": 15, + "total_ms": 848 + } + } + }, + { + "case": "접수 현황 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 924, + "review_ms": 12, + "total_ms": 971 + } + } + }, + { + "case": "접수 현황 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1001, + "review_ms": 45, + "total_ms": 1096 + } + } + }, + { + "case": "규칙으로 모르면 분류 + 답 두 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.6, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 23, + "classify_ms": 723, + "answer_ms": 838, + "review_ms": 15, + "total_ms": 1606 + } + } + }, + { + "case": "규칙으로 모르면 분류 + 답 두 번", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 1.4, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 594, + "answer_ms": 698, + "review_ms": 13, + "total_ms": 1339 + } + } + }, + { + "case": "규칙으로 모르면 분류 + 답 두 번", + "webhook": "my-helpdesk", + "run": 3, + "passed": true, + "problems": [], + "seconds": 1.6, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 746, + "answer_ms": 801, + "review_ms": 15, + "total_ms": 1599 + } + } + }, + { + "case": "빈 질문은 LLM 없이", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + }, + { + "case": "빈 질문은 LLM 없이", + "webhook": "my-helpdesk", + "run": 2, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 400, + "request": { + "question": "", + "user": "이준호" + }, + "response": { + "error": "question is required" + } + }, + { + "case": "빈 질문은 LLM 없이", + "webhook": "my-helpdesk", + "run": 3, + "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/72/answers/20-lab02-challenge.txt b/Last3Mon/72/answers/20-lab02-challenge.txt new file mode 100644 index 0000000..2344636 --- /dev/null +++ b/Last3Mon/72/answers/20-lab02-challenge.txt @@ -0,0 +1,397 @@ +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.4s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 3 +✔ VPN · LLM 한 번 · 2.5초 안 #1 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 37, + "answer_ms": 986, + "review_ms": 11, + "total_ms": 1046 + } + } +✔ VPN · LLM 한 번 · 2.5초 안 #2 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1057, + "review_ms": 14, + "total_ms": 1107 + } + } +✔ VPN · LLM 한 번 · 2.5초 안 #3 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 41, + "answer_ms": 952, + "review_ms": 20, + "total_ms": 1023 + } + } +✔ 비밀번호 · LLM 한 번 #1 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 81, + "answer_ms": 868, + "review_ms": 16, + "total_ms": 974 + } + } +✔ 비밀번호 · LLM 한 번 #2 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 824, + "review_ms": 15, + "total_ms": 881 + } + } +✔ 비밀번호 · LLM 한 번 #3 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 48, + "answer_ms": 811, + "review_ms": 12, + "total_ms": 879 + } + } +✔ 접수 현황 · LLM 한 번 #1 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 798, + "review_ms": 15, + "total_ms": 848 + } + } +✔ 접수 현황 · LLM 한 번 #2 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 924, + "review_ms": 12, + "total_ms": 971 + } + } +✔ 접수 현황 · LLM 한 번 #3 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1001, + "review_ms": 45, + "total_ms": 1096 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #1 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 23, + "classify_ms": 723, + "answer_ms": 838, + "review_ms": 15, + "total_ms": 1606 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #2 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 594, + "answer_ms": 698, + "review_ms": 13, + "total_ms": 1339 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #3 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 746, + "answer_ms": 801, + "review_ms": 15, + "total_ms": 1599 + } + } +✔ 빈 질문은 LLM 없이 #1 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +✔ 빈 질문은 LLM 없이 #2 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +✔ 빈 질문은 LLM 없이 #3 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 15/15 +report: runs/lab02-challenge-20261024-174837.json diff --git a/Last3Mon/72/answers/21-lab02-after-challenge.json b/Last3Mon/72/answers/21-lab02-after-challenge.json new file mode 100644 index 0000000..ea388c2 --- /dev/null +++ b/Last3Mon/72/answers/21-lab02-after-challenge.json @@ -0,0 +1,319 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 999, + "review_ms": 15, + "total_ms": 1047 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 802, + "review_ms": 50, + "total_ms": 899 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 901, + "review_ms": 15, + "total_ms": 951 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.6, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 572, + "answer_ms": 898, + "review_ms": 14, + "total_ms": 1522 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1106, + "review_ms": 11, + "total_ms": 1149 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 22, + "answer_ms": 1013, + "review_ms": 21, + "total_ms": 1063 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 989, + "review_ms": 33, + "total_ms": 1069 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 24, + "answer_ms": 786, + "review_ms": 17, + "total_ms": 838 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/21-lab02-after-challenge.txt b/Last3Mon/72/answers/21-lab02-after-challenge.txt new file mode 100644 index 0000000..ab34aec --- /dev/null +++ b/Last3Mon/72/answers/21-lab02-after-challenge.txt @@ -0,0 +1,261 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 999, + "review_ms": 15, + "total_ms": 1047 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 802, + "review_ms": 50, + "total_ms": 899 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 901, + "review_ms": 15, + "total_ms": 951 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 572, + "answer_ms": 898, + "review_ms": 14, + "total_ms": 1522 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1106, + "review_ms": 11, + "total_ms": 1149 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 22, + "answer_ms": 1013, + "review_ms": 21, + "total_ms": 1063 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 989, + "review_ms": 33, + "total_ms": 1069 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 24, + "answer_ms": 786, + "review_ms": 17, + "total_ms": 838 + } + } +Passed 9/9 +report: runs/lab02-20261024-174846.json diff --git a/Last3Mon/72/answers/22-lab01-after-challenge.json b/Last3Mon/72/answers/22-lab01-after-challenge.json new file mode 100644 index 0000000..b8261ff --- /dev/null +++ b/Last3Mon/72/answers/22-lab01-after-challenge.json @@ -0,0 +1,132 @@ +{ + "cases": "lab01.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "VPN FAQ · 단계별 시간", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1122, + "review_ms": 11, + "total_ms": 1173 + } + } + }, + { + "case": "VPN FAQ · 전체 시간 · LLM 호출 수", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1219, + "review_ms": 14, + "total_ms": 1268 + } + } + }, + { + "case": "접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 907, + "review_ms": 15, + "total_ms": 956 + } + } + }, + { + "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/72/answers/22-lab01-after-challenge.txt b/Last3Mon/72/answers/22-lab01-after-challenge.txt new file mode 100644 index 0000000..25a51ba --- /dev/null +++ b/Last3Mon/72/answers/22-lab01-after-challenge.txt @@ -0,0 +1,103 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1122, + "review_ms": 11, + "total_ms": 1173 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1219, + "review_ms": 14, + "total_ms": 1268 + } + } +✔ 접수 현황 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 907, + "review_ms": 15, + "total_ms": 956 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174849.json diff --git a/Last3Mon/72/answers/23-bench-after.txt b/Last3Mon/72/answers/23-bench-after.txt new file mode 100644 index 0000000..2c2db4c --- /dev/null +++ b/Last3Mon/72/answers/23-bench-after.txt @@ -0,0 +1,10 @@ +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.32s · route=자동 답변, llm_calls=1, cached=false +✔ #2 · HTTP 200 · 1.14s · route=자동 답변, llm_calls=1, cached=false +✔ #3 · HTTP 200 · 1.11s · route=자동 답변, llm_calls=1, cached=false +✔ #4 · HTTP 200 · 1.12s · route=자동 답변, llm_calls=1, cached=false +✔ #5 · HTTP 200 · 1.05s · route=자동 답변, llm_calls=1, cached=false +p50 1.12s · p95 1.32s · max 1.32s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174855.json + diff --git a/Last3Mon/72/answers/24-lab02-challenge2-before.json b/Last3Mon/72/answers/24-lab02-challenge2-before.json new file mode 100644 index 0000000..f68c52b --- /dev/null +++ b/Last3Mon/72/answers/24-lab02-challenge2-before.json @@ -0,0 +1,223 @@ +{ + "cases": "lab02-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 6, + "results": [ + { + "case": "처음 묻는 VPN (캐시 없음)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 39, + "answer_ms": 947, + "review_ms": 16, + "total_ms": 1009 + } + } + }, + { + "case": "다른 말로 묻는 VPN → FAQ 번호 캐시", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "took 1.2s (expected at most 1s)", + "cached = false (expected true)", + "cache_by = null (expected faq)", + "llm_calls = 1 (expected 0)" + ], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1067, + "review_ms": 14, + "total_ms": 1117 + } + } + }, + { + "case": "같은 말은 질문 캐시", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "cache_by = null (expected question)" + ], + "seconds": 0.1, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 14 + } + } + }, + { + "case": "no_cache면 FAQ 번호 캐시도 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1014, + "review_ms": 9, + "total_ms": 1051 + } + } + }, + { + "case": "다른 FAQ는 따로 (프린터 처음)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1091 + } + } + }, + { + "case": "접수 현황은 캐시 안 함", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 869, + "review_ms": 13, + "total_ms": 913 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/24-lab02-challenge2-before.txt b/Last3Mon/72/answers/24-lab02-challenge2-before.txt new file mode 100644 index 0000000..7061e0b --- /dev/null +++ b/Last3Mon/72/answers/24-lab02-challenge2-before.txt @@ -0,0 +1,176 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 처음 묻는 VPN (캐시 없음) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 39, + "answer_ms": 947, + "review_ms": 16, + "total_ms": 1009 + } + } +✘ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 1.2s · took 1.2s (expected at most 1s); cached = false (expected true); cache_by = null (expected faq); llm_calls = 1 (expected 0) + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1067, + "review_ms": 14, + "total_ms": 1117 + } + } +✘ 같은 말은 질문 캐시 · 0.1s · cache_by = null (expected question) + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 14 + } + } +✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1014, + "review_ms": 9, + "total_ms": 1051 + } + } +✔ 다른 FAQ는 따로 (프린터 처음) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1091 + } + } +✔ 접수 현황은 캐시 안 함 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 869, + "review_ms": 13, + "total_ms": 913 + } + } +Passed 4/6 +report: runs/lab02-challenge2-20261024-174901.json diff --git a/Last3Mon/72/answers/25-lab02-challenge2.json b/Last3Mon/72/answers/25-lab02-challenge2.json new file mode 100644 index 0000000..8200a09 --- /dev/null +++ b/Last3Mon/72/answers/25-lab02-challenge2.json @@ -0,0 +1,217 @@ +{ + "cases": "lab02-challenge2.json", + "webhook": "my-helpdesk", + "passed": 6, + "total": 6, + "results": [ + { + "case": "처음 묻는 VPN (캐시 없음)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.5, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1556, + "review_ms": 23, + "total_ms": 1665 + } + } + }, + { + "case": "다른 말로 묻는 VPN → FAQ 번호 캐시", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "faq", + "steps": [ + "faq", + "rule", + "cache" + ], + "timings": { + "total_ms": 44 + } + } + }, + { + "case": "같은 말은 질문 캐시", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } + }, + { + "case": "no_cache면 FAQ 번호 캐시도 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1052, + "review_ms": 20, + "total_ms": 1110 + } + } + }, + { + "case": "다른 FAQ는 따로 (프린터 처음)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 42, + "answer_ms": 1035, + "review_ms": 14, + "total_ms": 1102 + } + } + }, + { + "case": "접수 현황은 캐시 안 함", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 34, + "answer_ms": 796, + "review_ms": 14, + "total_ms": 852 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/25-lab02-challenge2.txt b/Last3Mon/72/answers/25-lab02-challenge2.txt new file mode 100644 index 0000000..229059d --- /dev/null +++ b/Last3Mon/72/answers/25-lab02-challenge2.txt @@ -0,0 +1,177 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 3} +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 처음 묻는 VPN (캐시 없음) · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1556, + "review_ms": 23, + "total_ms": 1665 + } + } +✔ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "faq", + "steps": [ + "faq", + "rule", + "cache" + ], + "timings": { + "total_ms": 44 + } + } +✔ 같은 말은 질문 캐시 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } +✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1052, + "review_ms": 20, + "total_ms": 1110 + } + } +✔ 다른 FAQ는 따로 (프린터 처음) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 42, + "answer_ms": 1035, + "review_ms": 14, + "total_ms": 1102 + } + } +✔ 접수 현황은 캐시 안 함 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 34, + "answer_ms": 796, + "review_ms": 14, + "total_ms": 852 + } + } +Passed 6/6 +report: runs/lab02-challenge2-20261024-174920.json diff --git a/Last3Mon/72/answers/26-lab02-challenge-after2.json b/Last3Mon/72/answers/26-lab02-challenge-after2.json new file mode 100644 index 0000000..f085153 --- /dev/null +++ b/Last3Mon/72/answers/26-lab02-challenge-after2.json @@ -0,0 +1,173 @@ +{ + "cases": "lab02-challenge.json", + "webhook": "my-helpdesk", + "passed": 5, + "total": 5, + "results": [ + { + "case": "VPN · LLM 한 번 · 2.5초 안", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1017, + "review_ms": 16, + "total_ms": 1067 + } + } + }, + { + "case": "비밀번호 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 840, + "review_ms": 40, + "total_ms": 928 + } + } + }, + { + "case": "접수 현황 · LLM 한 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 835, + "review_ms": 19, + "total_ms": 891 + } + } + }, + { + "case": "규칙으로 모르면 분류 + 답 두 번", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 842, + "answer_ms": 771, + "review_ms": 11, + "total_ms": 1659 + } + } + }, + { + "case": "빈 질문은 LLM 없이", + "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/72/answers/26-lab02-challenge-after2.txt b/Last3Mon/72/answers/26-lab02-challenge-after2.txt new file mode 100644 index 0000000..53113dd --- /dev/null +++ b/Last3Mon/72/answers/26-lab02-challenge-after2.txt @@ -0,0 +1,139 @@ +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ VPN · LLM 한 번 · 2.5초 안 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1017, + "review_ms": 16, + "total_ms": 1067 + } + } +✔ 비밀번호 · LLM 한 번 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 840, + "review_ms": 40, + "total_ms": 928 + } + } +✔ 접수 현황 · LLM 한 번 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 835, + "review_ms": 19, + "total_ms": 891 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 842, + "answer_ms": 771, + "review_ms": 11, + "total_ms": 1659 + } + } +✔ 빈 질문은 LLM 없이 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 5/5 +report: runs/lab02-challenge-20261024-174926.json diff --git a/Last3Mon/72/answers/27-lab02-after2.json b/Last3Mon/72/answers/27-lab02-after2.json new file mode 100644 index 0000000..28913fa --- /dev/null +++ b/Last3Mon/72/answers/27-lab02-after2.json @@ -0,0 +1,328 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1111, + "review_ms": 17, + "total_ms": 1168 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 895, + "review_ms": 16, + "total_ms": 945 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 946, + "review_ms": 17, + "total_ms": 998 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "classify_ms": 623, + "answer_ms": 1026, + "review_ms": 14, + "total_ms": 1699 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1112, + "review_ms": 23, + "total_ms": 1168 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 15 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1065, + "review_ms": 32, + "total_ms": 1137 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 936, + "review_ms": 15, + "total_ms": 988 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.9, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 822, + "review_ms": 14, + "total_ms": 873 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/27-lab02-after2.txt b/Last3Mon/72/answers/27-lab02-after2.txt new file mode 100644 index 0000000..a6d78c2 --- /dev/null +++ b/Last3Mon/72/answers/27-lab02-after2.txt @@ -0,0 +1,270 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1111, + "review_ms": 17, + "total_ms": 1168 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 895, + "review_ms": 16, + "total_ms": 945 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 946, + "review_ms": 17, + "total_ms": 998 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "classify_ms": 623, + "answer_ms": 1026, + "review_ms": 14, + "total_ms": 1699 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1112, + "review_ms": 23, + "total_ms": 1168 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.0s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 15 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1065, + "review_ms": 32, + "total_ms": 1137 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 936, + "review_ms": 15, + "total_ms": 988 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 822, + "review_ms": 14, + "total_ms": 873 + } + } +Passed 9/9 +report: runs/lab02-20261024-174935.json diff --git a/Last3Mon/72/answers/28-lab03-fault-before.txt b/Last3Mon/72/answers/28-lab03-fault-before.txt new file mode 100644 index 0000000..860492a --- /dev/null +++ b/Last3Mon/72/answers/28-lab03-fault-before.txt @@ -0,0 +1,53 @@ +$ uv run tools/flow.py fault faq slow +✔ fault switch: {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} + +$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +HTTP 200 · 11.5s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 8144, + "answer_ms": 1518, + "review_ms": 1013, + "total_ms": 10701 + } + } + +$ uv run tools/flow.py fault faq error +✔ fault switch: {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} + +$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +HTTP 500 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } + +$ uv run tools/flow.py fault all ok +✔ fault switch: {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} + diff --git a/Last3Mon/72/answers/29-lab03.json b/Last3Mon/72/answers/29-lab03.json new file mode 100644 index 0000000..c2d7b70 --- /dev/null +++ b/Last3Mon/72/answers/29-lab03.json @@ -0,0 +1,112 @@ +{ + "cases": "lab03.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 605, + "answer_ms": 1634, + "review_ms": 13, + "total_ms": 2270 + } + } + }, + { + "case": "FAQ가 느릴 때 (8초)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ가 오류일 때 (500)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "캐시에 있으면 FAQ가 멈춰도 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/29-lab03.txt b/Last3Mon/72/answers/29-lab03.txt new file mode 100644 index 0000000..f0003e7 --- /dev/null +++ b/Last3Mon/72/answers/29-lab03.txt @@ -0,0 +1,92 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 3.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 605, + "answer_ms": 1634, + "review_ms": 13, + "total_ms": 2270 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175023.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/30-lab02-resilient.json b/Last3Mon/72/answers/30-lab02-resilient.json new file mode 100644 index 0000000..2afd999 --- /dev/null +++ b/Last3Mon/72/answers/30-lab02-resilient.json @@ -0,0 +1,319 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.8, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 585, + "answer_ms": 1071, + "review_ms": 20, + "total_ms": 1687 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1030, + "review_ms": 19, + "total_ms": 1128 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.8, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 555, + "answer_ms": 1179, + "review_ms": 24, + "total_ms": 1774 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 554, + "classify_ms": 650, + "answer_ms": 927, + "review_ms": 14, + "total_ms": 2152 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 553, + "answer_ms": 1015, + "review_ms": 28, + "total_ms": 1605 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 551, + "answer_ms": 1081, + "review_ms": 34, + "total_ms": 1685 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1062, + "review_ms": 14, + "total_ms": 1114 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 47, + "answer_ms": 977, + "review_ms": 15, + "total_ms": 1049 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/30-lab02-resilient.txt b/Last3Mon/72/answers/30-lab02-resilient.txt new file mode 100644 index 0000000..33571b1 --- /dev/null +++ b/Last3Mon/72/answers/30-lab02-resilient.txt @@ -0,0 +1,261 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 585, + "answer_ms": 1071, + "review_ms": 20, + "total_ms": 1687 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1030, + "review_ms": 19, + "total_ms": 1128 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 555, + "answer_ms": 1179, + "review_ms": 24, + "total_ms": 1774 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 554, + "classify_ms": 650, + "answer_ms": 927, + "review_ms": 14, + "total_ms": 2152 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 553, + "answer_ms": 1015, + "review_ms": 28, + "total_ms": 1605 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 551, + "answer_ms": 1081, + "review_ms": 34, + "total_ms": 1685 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1062, + "review_ms": 14, + "total_ms": 1114 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 47, + "answer_ms": 977, + "review_ms": 15, + "total_ms": 1049 + } + } +Passed 9/9 +report: runs/lab02-20261024-175036.json diff --git a/Last3Mon/72/answers/31-llm-down-before.txt b/Last3Mon/72/answers/31-llm-down-before.txt new file mode 100644 index 0000000..58cef2d --- /dev/null +++ b/Last3Mon/72/answers/31-llm-down-before.txt @@ -0,0 +1,19 @@ +$ uv run tools/flow.py llm down +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up + +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn-nocache.json -v +HTTP 500 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } + +$ uv run tools/flow.py llm up +✔ OpenAI credential: real key from .env restored + diff --git a/Last3Mon/72/answers/32-lab03-llm-down-before.json b/Last3Mon/72/answers/32-lab03-llm-down-before.json new file mode 100644 index 0000000..bbc7bd0 --- /dev/null +++ b/Last3Mon/72/answers/32-lab03-llm-down-before.json @@ -0,0 +1,84 @@ +{ + "cases": "lab03-llm-down.json", + "webhook": "my-helpdesk", + "passed": 1, + "total": 4, + "results": [ + { + "case": "VPN · FAQ 원문으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 500 (expected 200)", + "route = null (expected 간단 답변)", + "answer has none of ['인증서']" + ], + "seconds": 0.9, + "status": 500, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "message": "Error in workflow" + } + }, + { + "case": "접수 현황 · 상태 그대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 500 (expected 200)", + "route = null (expected 간단 답변)", + "answer has none of ['처리 중']" + ], + "seconds": 0.4, + "status": 500, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "message": "Error in workflow" + } + }, + { + "case": "모르는 질문 · 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "status 500 (expected 200)", + "route = null (expected 담당자 연결)" + ], + "seconds": 0.9, + "status": 500, + "request": { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + }, + "response": { + "message": "Error in workflow" + } + }, + { + "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/72/answers/32-lab03-llm-down-before.txt b/Last3Mon/72/answers/32-lab03-llm-down-before.txt new file mode 100644 index 0000000..ba19080 --- /dev/null +++ b/Last3Mon/72/answers/32-lab03-llm-down-before.txt @@ -0,0 +1,47 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✘ VPN · FAQ 원문으로 · 0.9s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✘ 접수 현황 · 상태 그대로 · 0.4s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✘ 모르는 질문 · 담당자 · 0.9s · status 500 (expected 200); route = null (expected 담당자 연결) + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/4 +report: runs/lab03-llm-down-20261024-175047.json +✔ OpenAI credential: real key from .env restored diff --git a/Last3Mon/72/answers/33-lab03-llm-down.json b/Last3Mon/72/answers/33-lab03-llm-down.json new file mode 100644 index 0000000..9197831 --- /dev/null +++ b/Last3Mon/72/answers/33-lab03-llm-down.json @@ -0,0 +1,90 @@ +{ + "cases": "lab03-llm-down.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "VPN · FAQ 원문으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "case": "접수 현황 · 상태 그대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.3, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "case": "모르는 질문 · 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.8, + "status": 200, + "request": { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + }, + "response": { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "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/72/answers/33-lab03-llm-down.txt b/Last3Mon/72/answers/33-lab03-llm-down.txt new file mode 100644 index 0000000..c510de2 --- /dev/null +++ b/Last3Mon/72/answers/33-lab03-llm-down.txt @@ -0,0 +1,64 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN · FAQ 원문으로 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 접수 현황 · 상태 그대로 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 모르는 질문 · 담당자 · 0.8s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab03-llm-down-20261024-175108.json +✔ OpenAI credential: real key from .env restored diff --git a/Last3Mon/72/answers/34-lab02-llm-fallback.json b/Last3Mon/72/answers/34-lab02-llm-fallback.json new file mode 100644 index 0000000..fe9ac3c --- /dev/null +++ b/Last3Mon/72/answers/34-lab02-llm-fallback.json @@ -0,0 +1,319 @@ +{ + "cases": "lab02.json", + "webhook": "my-helpdesk", + "passed": 9, + "total": 9, + "results": [ + { + "case": "예제 1 · 프린터 FAQ는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.8, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 568, + "answer_ms": 1078, + "review_ms": 12, + "total_ms": 1679 + } + } + }, + { + "case": "예제 1 · 접수 번호는 규칙으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 969, + "review_ms": 15, + "total_ms": 1032 + } + } + }, + { + "case": "예제 1 · 없는 접수 번호", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드리겠습니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 546, + "answer_ms": 1011, + "review_ms": 46, + "total_ms": 1632 + } + } + }, + { + "case": "예제 1 · 규칙으로 모르면 LLM이 분류", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "classify_ms": 881, + "answer_ms": 734, + "review_ms": 23, + "total_ms": 2206 + } + } + }, + { + "case": "예제 2 · 처음 묻는 VPN", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.9, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1222, + "review_ms": 12, + "total_ms": 1803 + } + } + }, + { + "case": "예제 2 · 같은 질문 (띄어쓰기·대소문자 다름)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 20 + } + } + }, + { + "case": "예제 2 · no_cache면 캐시 안 씀", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1625 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (1)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 934, + "review_ms": 19, + "total_ms": 986 + } + } + }, + { + "case": "예제 2 · 접수 현황은 캐시 안 함 (2)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1016, + "review_ms": 43, + "total_ms": 1110 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/34-lab02-llm-fallback.txt b/Last3Mon/72/answers/34-lab02-llm-fallback.txt new file mode 100644 index 0000000..a8836f6 --- /dev/null +++ b/Last3Mon/72/answers/34-lab02-llm-fallback.txt @@ -0,0 +1,261 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 568, + "answer_ms": 1078, + "review_ms": 12, + "total_ms": 1679 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 969, + "review_ms": 15, + "total_ms": 1032 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드리겠습니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 546, + "answer_ms": 1011, + "review_ms": 46, + "total_ms": 1632 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "classify_ms": 881, + "answer_ms": 734, + "review_ms": 23, + "total_ms": 2206 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1222, + "review_ms": 12, + "total_ms": 1803 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 20 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1625 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 934, + "review_ms": 19, + "total_ms": 986 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1016, + "review_ms": 43, + "total_ms": 1110 + } + } +Passed 9/9 +report: runs/lab02-20261024-175123.json diff --git a/Last3Mon/72/answers/35-lab03-llm-fallback.json b/Last3Mon/72/answers/35-lab03-llm-fallback.json new file mode 100644 index 0000000..0976db8 --- /dev/null +++ b/Last3Mon/72/answers/35-lab03-llm-fallback.json @@ -0,0 +1,112 @@ +{ + "cases": "lab03.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 571, + "answer_ms": 1006, + "review_ms": 16, + "total_ms": 1600 + } + } + }, + { + "case": "FAQ가 느릴 때 (8초)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ가 오류일 때 (500)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "캐시에 있으면 FAQ가 멈춰도 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/35-lab03-llm-fallback.txt b/Last3Mon/72/answers/35-lab03-llm-fallback.txt new file mode 100644 index 0000000..87a7cea --- /dev/null +++ b/Last3Mon/72/answers/35-lab03-llm-fallback.txt @@ -0,0 +1,92 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 571, + "answer_ms": 1006, + "review_ms": 16, + "total_ms": 1600 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.9s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175132.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/36-lab03-ticket-before.json b/Last3Mon/72/answers/36-lab03-ticket-before.json new file mode 100644 index 0000000..5bc5d27 --- /dev/null +++ b/Last3Mon/72/answers/36-lab03-ticket-before.json @@ -0,0 +1,119 @@ +{ + "cases": "lab03-ticket.json", + "webhook": "my-helpdesk", + "passed": 2, + "total": 4, + "results": [ + { + "case": "평소 접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 915, + "review_ms": 12, + "total_ms": 970 + } + } + }, + { + "case": "접수 API 500 · 접수 질문은 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "reason = data_api_down (expected tickets_down)" + ], + "seconds": 0.6, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "접수 API 500 · FAQ 질문은 그대로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "route = 담당자 연결 (expected 자동 답변)", + "category = null (expected faq)", + "answer has none of ['인증서']" + ], + "seconds": 0.6, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "없는 접수 번호는 그대로 (404는 고장이 아님)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.6, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 564, + "answer_ms": 978, + "review_ms": 10, + "total_ms": 1558 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/36-lab03-ticket-before.txt b/Last3Mon/72/answers/36-lab03-ticket-before.txt new file mode 100644 index 0000000..6f250cc --- /dev/null +++ b/Last3Mon/72/answers/36-lab03-ticket-before.txt @@ -0,0 +1,90 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 접수 현황 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 915, + "review_ms": 12, + "total_ms": 970 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✘ 접수 API 500 · 접수 질문은 담당자 · 0.6s · reason = data_api_down (expected tickets_down) + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✘ 접수 API 500 · FAQ 질문은 그대로 답 · 0.6s · route = 담당자 연결 (expected 자동 답변); category = null (expected faq); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 564, + "answer_ms": 978, + "review_ms": 10, + "total_ms": 1558 + } + } +Passed 2/4 +report: runs/lab03-ticket-20261024-175136.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/37-lab03-ticket.json b/Last3Mon/72/answers/37-lab03-ticket.json new file mode 100644 index 0000000..bdbe293 --- /dev/null +++ b/Last3Mon/72/answers/37-lab03-ticket.json @@ -0,0 +1,133 @@ +{ + "cases": "lab03-ticket.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소 접수 현황", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.4, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 102, + "answer_ms": 1458, + "review_ms": 13, + "total_ms": 1594 + } + } + }, + { + "case": "접수 API 500 · 접수 질문은 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "담당자 연결", + "category": "ticket", + "answer": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", + "reason": "tickets_down", + "llm_calls": 0 + } + }, + { + "case": "접수 API 500 · FAQ 질문은 그대로 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 1023, + "review_ms": 12, + "total_ms": 1083 + } + } + }, + { + "case": "없는 접수 번호는 그대로 (404는 고장이 아님)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + }, + "response": { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 44, + "answer_ms": 1192, + "review_ms": 22, + "total_ms": 1270 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/37-lab03-ticket.txt b/Last3Mon/72/answers/37-lab03-ticket.txt new file mode 100644 index 0000000..191571e --- /dev/null +++ b/Last3Mon/72/answers/37-lab03-ticket.txt @@ -0,0 +1,110 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 접수 현황 · 2.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 102, + "answer_ms": 1458, + "review_ms": 13, + "total_ms": 1594 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✔ 접수 API 500 · 접수 질문은 담당자 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "ticket", + "answer": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", + "reason": "tickets_down", + "llm_calls": 0 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✔ 접수 API 500 · FAQ 질문은 그대로 답 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 1023, + "review_ms": 12, + "total_ms": 1083 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 44, + "answer_ms": 1192, + "review_ms": 22, + "total_ms": 1270 + } + } +Passed 4/4 +report: runs/lab03-ticket-20261024-175154.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/38-lab03-after-ticket.json b/Last3Mon/72/answers/38-lab03-after-ticket.json new file mode 100644 index 0000000..d4f1256 --- /dev/null +++ b/Last3Mon/72/answers/38-lab03-after-ticket.json @@ -0,0 +1,112 @@ +{ + "cases": "lab03.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": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 61, + "answer_ms": 1037, + "review_ms": 14, + "total_ms": 1120 + } + } + }, + { + "case": "FAQ가 느릴 때 (8초)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.8, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ가 오류일 때 (500)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "캐시에 있으면 FAQ가 멈춰도 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.0, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 17 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/38-lab03-after-ticket.txt b/Last3Mon/72/answers/38-lab03-after-ticket.txt new file mode 100644 index 0000000..5fddfdc --- /dev/null +++ b/Last3Mon/72/answers/38-lab03-after-ticket.txt @@ -0,0 +1,92 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 61, + "answer_ms": 1037, + "review_ms": 14, + "total_ms": 1120 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.8s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 17 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175202.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/39-lab03-llm-down-after-ticket.json b/Last3Mon/72/answers/39-lab03-llm-down-after-ticket.json new file mode 100644 index 0000000..6c6a6db --- /dev/null +++ b/Last3Mon/72/answers/39-lab03-llm-down-after-ticket.json @@ -0,0 +1,90 @@ +{ + "cases": "lab03-llm-down.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "VPN · FAQ 원문으로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "case": "접수 현황 · 상태 그대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.5, + "status": 200, + "request": { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + }, + "response": { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "case": "모르는 질문 · 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.3, + "status": 200, + "request": { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + }, + "response": { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } + }, + { + "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/72/answers/39-lab03-llm-down-after-ticket.txt b/Last3Mon/72/answers/39-lab03-llm-down-after-ticket.txt new file mode 100644 index 0000000..aa56eae --- /dev/null +++ b/Last3Mon/72/answers/39-lab03-llm-down-after-ticket.txt @@ -0,0 +1,64 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN · FAQ 원문으로 · 0.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 접수 현황 · 상태 그대로 · 0.5s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 모르는 질문 · 담당자 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab03-llm-down-20261024-175206.json +✔ OpenAI credential: real key from .env restored diff --git a/Last3Mon/72/answers/40-lab03-challenge.json b/Last3Mon/72/answers/40-lab03-challenge.json new file mode 100644 index 0000000..d5bf417 --- /dev/null +++ b/Last3Mon/72/answers/40-lab03-challenge.json @@ -0,0 +1,94 @@ +{ + "cases": "lab03-challenge.json", + "webhook": "my-health", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소", + "webhook": "my-health", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.2, + "status": 200, + "request": {}, + "response": { + "status": "ok", + "checks": { + "data_api": true, + "faq": true, + "llm": true + }, + "failed": [], + "seconds": 1.4 + } + }, + { + "case": "FAQ 오류", + "webhook": "my-health", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.0, + "status": 200, + "request": {}, + "response": { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 1 + } + }, + { + "case": "FAQ 느림 (8초) · 점검은 빨리 끝남", + "webhook": "my-health", + "run": 1, + "passed": true, + "problems": [], + "seconds": 3.0, + "status": 200, + "request": {}, + "response": { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 2.9 + } + }, + { + "case": "LLM 멈춤", + "webhook": "my-health", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.4, + "status": 200, + "request": {}, + "response": { + "status": "degraded", + "checks": { + "data_api": true, + "faq": true, + "llm": false + }, + "failed": [ + "llm" + ], + "seconds": 0.3 + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/40-lab03-challenge.txt b/Last3Mon/72/answers/40-lab03-challenge.txt new file mode 100644 index 0000000..001be28 --- /dev/null +++ b/Last3Mon/72/answers/40-lab03-challenge.txt @@ -0,0 +1,72 @@ +POST http://localhost:5678/webhook/my-health · 4 case(s) × 1 +✔ 평소 · 2.2s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "ok", + "checks": { + "data_api": true, + "faq": true, + "llm": true + }, + "failed": [], + "seconds": 1.4 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 오류 · 1.0s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 1 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 (8초) · 점검은 빨리 끝남 · 3.0s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 2.9 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +✔ LLM 멈춤 · 0.4s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": true, + "llm": false + }, + "failed": [ + "llm" + ], + "seconds": 0.3 + } +✔ OpenAI credential: real key from .env restored +Passed 4/4 +report: runs/lab03-challenge-20261024-175230.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/41-lab03-challenge2-before.json b/Last3Mon/72/answers/41-lab03-challenge2-before.json new file mode 100644 index 0000000..44a8491 --- /dev/null +++ b/Last3Mon/72/answers/41-lab03-challenge2-before.json @@ -0,0 +1,123 @@ +{ + "cases": "lab03-challenge2.json", + "webhook": "my-helpdesk", + "passed": 3, + "total": 4, + "results": [ + { + "case": "평소", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.3, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 72, + "answer_ms": 1098, + "review_ms": 18, + "total_ms": 1212 + } + } + }, + { + "case": "FAQ 느림 · 처음 한 번은 기다린 뒤 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림)", + "webhook": "my-helpdesk", + "run": 1, + "passed": false, + "problems": [ + "took 4.6s (expected at most 1s)", + "reason = data_api_down (expected breaker_open)" + ], + "seconds": 4.6, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ가 돌아오고 차단기를 비우면 평소대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.2, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 43, + "answer_ms": 1092, + "review_ms": 15, + "total_ms": 1161 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/41-lab03-challenge2-before.txt b/Last3Mon/72/answers/41-lab03-challenge2-before.txt new file mode 100644 index 0000000..7df8690 --- /dev/null +++ b/Last3Mon/72/answers/41-lab03-challenge2-before.txt @@ -0,0 +1,97 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 72, + "answer_ms": 1098, + "review_ms": 18, + "total_ms": 1212 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✘ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 4.6s · took 4.6s (expected at most 1s); reason = data_api_down (expected breaker_open) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 43, + "answer_ms": 1092, + "review_ms": 15, + "total_ms": 1161 + } + } +Passed 3/4 +report: runs/lab03-challenge2-20261024-175242.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} diff --git a/Last3Mon/72/answers/42-lab03-challenge2.json b/Last3Mon/72/answers/42-lab03-challenge2.json new file mode 100644 index 0000000..043156d --- /dev/null +++ b/Last3Mon/72/answers/42-lab03-challenge2.json @@ -0,0 +1,121 @@ +{ + "cases": "lab03-challenge2.json", + "webhook": "my-helpdesk", + "passed": 4, + "total": 4, + "results": [ + { + "case": "평소", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 2.7, + "status": 200, + "request": { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 114, + "answer_ms": 1678, + "review_ms": 14, + "total_ms": 1827 + } + } + }, + { + "case": "FAQ 느림 · 처음 한 번은 기다린 뒤 담당자", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.8, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } + }, + { + "case": "FAQ가 돌아오고 차단기를 비우면 평소대로", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 1.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 52, + "answer_ms": 1005, + "review_ms": 18, + "total_ms": 1084 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/42-lab03-challenge2.txt b/Last3Mon/72/answers/42-lab03-challenge2.txt new file mode 100644 index 0000000..a1e95a7 --- /dev/null +++ b/Last3Mon/72/answers/42-lab03-challenge2.txt @@ -0,0 +1,98 @@ +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 114, + "answer_ms": 1678, + "review_ms": 14, + "total_ms": 1827 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.8s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 52, + "answer_ms": 1005, + "review_ms": 18, + "total_ms": 1084 + } + } +Passed 4/4 +report: runs/lab03-challenge2-20261024-175304.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} diff --git a/Last3Mon/72/answers/43-lab03-after-breaker.json b/Last3Mon/72/answers/43-lab03-after-breaker.json new file mode 100644 index 0000000..daf6871 --- /dev/null +++ b/Last3Mon/72/answers/43-lab03-after-breaker.json @@ -0,0 +1,113 @@ +{ + "cases": "lab03.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": "이준호", + "no_cache": true + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 77, + "answer_ms": 1000, + "review_ms": 16, + "total_ms": 1104 + } + } + }, + { + "case": "FAQ가 느릴 때 (8초)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 4.6, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } + }, + { + "case": "FAQ가 오류일 때 (500)", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + }, + "response": { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } + }, + { + "case": "캐시에 있으면 FAQ가 멈춰도 답", + "webhook": "my-helpdesk", + "run": 1, + "passed": true, + "problems": [], + "seconds": 0.1, + "status": 200, + "request": { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + }, + "response": { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } + } + ] +} \ No newline at end of file diff --git a/Last3Mon/72/answers/43-lab03-after-breaker.txt b/Last3Mon/72/answers/43-lab03-after-breaker.txt new file mode 100644 index 0000000..16e79d1 --- /dev/null +++ b/Last3Mon/72/answers/43-lab03-after-breaker.txt @@ -0,0 +1,93 @@ +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 77, + "answer_ms": 1000, + "review_ms": 16, + "total_ms": 1104 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175311.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} diff --git a/Last3Mon/72/answers/44-doctor.txt b/Last3Mon/72/answers/44-doctor.txt new file mode 100644 index 0000000..faa5092 --- /dev/null +++ b/Last3Mon/72/answers/44-doctor.txt @@ -0,0 +1,13 @@ +$ 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 +✔ fault switch /admin/fault (day 72 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/72/answers/72-lab01-challenge-timings.json b/Last3Mon/72/answers/72-lab01-challenge-timings.json new file mode 100644 index 0000000..2080a60 --- /dev/null +++ b/Last3Mon/72/answers/72-lab01-challenge-timings.json @@ -0,0 +1,586 @@ +{ + "name": "72 IT helpdesk + step timings (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": "f7a423a6-a6a2-57a3-954f-e8540a1ce022" + }, + { + "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": "6dce76b0-2dd0-5181-99be-b0b894c87c8b" + }, + { + "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": "75371917-2483-583e-9135-5c663a18b08b" + }, + { + "name": "Start clock", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t: Date.now() } }];" + }, + "position": [ + 440, + 300 + ], + "id": "34377f95-81c5-5aa3-9e2c-6b63a8e5ae54" + }, + { + "name": "Classify", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "={{ $('Question webhook').first().json.body.question }}", + "options": { + "systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}", + "maxIterations": 1 + } + }, + "position": [ + 660, + 300 + ], + "id": "7a93b236-dbff-52c5-9397-61087bc0688d" + }, + { + "name": "Classify 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": [ + 660, + 520 + ], + "id": "357e7611-4597-5ba3-a6ba-9a4d4d63cda7" + }, + { + "name": "Parse category", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];" + }, + "position": [ + 880, + 300 + ], + "id": "9327634c-6764-5403-b7bc-3e666e7ae3d6" + }, + { + "name": "After classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1100, + 300 + ], + "id": "8f3da4f6-ce34-5505-b543-e5d68d990a96" + }, + { + "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": [ + 1320, + 300 + ], + "id": "103c6254-01ea-5e80-81bb-7e3b9ea7beb5" + }, + { + "name": "After FAQ", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1540, + 300 + ], + "id": "d03181b0-715d-5fae-98a0-92c52a2b3b3c" + }, + { + "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": [ + 1760, + 300 + ], + "id": "f20b5c78-0224-5d55-8d77-0a9e98061a6b" + }, + { + "name": "After data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1980, + 300 + ], + "id": "cca89aef-ef17-5b5f-a3c3-a53e0d762b43" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $('Parse category').first().json.category }}\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.", + "maxIterations": 1 + } + }, + "position": [ + 2200, + 300 + ], + "id": "d7ec1c62-265f-5d16-b67a-c5922305bb8a" + }, + { + "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": [ + 2200, + 520 + ], + "id": "5c7f8b81-cd4c-59a2-9899-9bdb4813f7ab" + }, + { + "name": "After answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2420, + 300 + ], + "id": "43dab328-4901-5fd6-8238-6e0f836cc31d" + }, + { + "name": "Review", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=초안: {{ $('Answer').first().json.output }}", + "options": { + "systemMessage": "You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.", + "maxIterations": 1 + } + }, + "position": [ + 2640, + 300 + ], + "id": "a4583946-f582-5ad8-963b-b866b74e9ba4" + }, + { + "name": "Review 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": [ + 2640, + 520 + ], + "id": "3be9acc9-4b3d-5a41-b6a4-00fa1b04d777" + }, + { + "name": "After review", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2860, + 300 + ], + "id": "d07d9992-7e5e-5aab-b925-9387021bbaff" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').first().json.category;\nconst answer = String($('Review').first().json.output ?? '').trim();\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('review');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 1;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['classify', t('After classify')], ['faq', t('After FAQ')], ['ticket', t('After data')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nconst parts = Object.entries(timings).filter(([key]) => key !== 'total_ms');\nresult.slowest_step = parts.sort((a, b) => b[1] - a[1])[0][0].replace(/_ms$/, '');\nreturn [{ json: result }];" + }, + "position": [ + 3080, + 300 + ], + "id": "079e44f0-da3e-57dd-be6a-87025b6a320f" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 3300, + 300 + ], + "id": "8ac7b99c-d9fa-5a1e-a168-33eb7ab5e2a3" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Start clock", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start clock": { + "main": [ + [ + { + "node": "Classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Classify model": { + "ai_languageModel": [ + [ + { + "node": "Classify", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Classify": { + "main": [ + [ + { + "node": "Parse category", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse category": { + "main": [ + [ + { + "node": "After classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "After classify": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "After FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "After FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "After data", + "type": "main", + "index": 0 + } + ] + ] + }, + "After data": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "After answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "After answer": { + "main": [ + [ + { + "node": "Review", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review model": { + "ai_languageModel": [ + [ + { + "node": "Review", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Review": { + "main": [ + [ + { + "node": "After review", + "type": "main", + "index": 0 + } + ] + ] + }, + "After review": { + "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/72/answers/72-lab01-challenge2-budget.json b/Last3Mon/72/answers/72-lab01-challenge2-budget.json new file mode 100644 index 0000000..a6a23a5 --- /dev/null +++ b/Last3Mon/72/answers/72-lab01-challenge2-budget.json @@ -0,0 +1,586 @@ +{ + "name": "72 IT helpdesk + time budget (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": "f7a423a6-a6a2-57a3-954f-e8540a1ce022" + }, + { + "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": "6dce76b0-2dd0-5181-99be-b0b894c87c8b" + }, + { + "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": "75371917-2483-583e-9135-5c663a18b08b" + }, + { + "name": "Start clock", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t: Date.now() } }];" + }, + "position": [ + 440, + 300 + ], + "id": "34377f95-81c5-5aa3-9e2c-6b63a8e5ae54" + }, + { + "name": "Classify", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "={{ $('Question webhook').first().json.body.question }}", + "options": { + "systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}", + "maxIterations": 1 + } + }, + "position": [ + 660, + 300 + ], + "id": "7a93b236-dbff-52c5-9397-61087bc0688d" + }, + { + "name": "Classify 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": [ + 660, + 520 + ], + "id": "357e7611-4597-5ba3-a6ba-9a4d4d63cda7" + }, + { + "name": "Parse category", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];" + }, + "position": [ + 880, + 300 + ], + "id": "9327634c-6764-5403-b7bc-3e666e7ae3d6" + }, + { + "name": "After classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1100, + 300 + ], + "id": "8f3da4f6-ce34-5505-b543-e5d68d990a96" + }, + { + "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": [ + 1320, + 300 + ], + "id": "103c6254-01ea-5e80-81bb-7e3b9ea7beb5" + }, + { + "name": "After FAQ", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1540, + 300 + ], + "id": "d03181b0-715d-5fae-98a0-92c52a2b3b3c" + }, + { + "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": [ + 1760, + 300 + ], + "id": "f20b5c78-0224-5d55-8d77-0a9e98061a6b" + }, + { + "name": "After data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1980, + 300 + ], + "id": "cca89aef-ef17-5b5f-a3c3-a53e0d762b43" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $('Parse category').first().json.category }}\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.", + "maxIterations": 1 + } + }, + "position": [ + 2200, + 300 + ], + "id": "d7ec1c62-265f-5d16-b67a-c5922305bb8a" + }, + { + "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": [ + 2200, + 520 + ], + "id": "5c7f8b81-cd4c-59a2-9899-9bdb4813f7ab" + }, + { + "name": "After answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2420, + 300 + ], + "id": "43dab328-4901-5fd6-8238-6e0f836cc31d" + }, + { + "name": "Review", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=초안: {{ $('Answer').first().json.output }}", + "options": { + "systemMessage": "You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.", + "maxIterations": 1 + } + }, + "position": [ + 2640, + 300 + ], + "id": "a4583946-f582-5ad8-963b-b866b74e9ba4" + }, + { + "name": "Review 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": [ + 2640, + 520 + ], + "id": "3be9acc9-4b3d-5a41-b6a4-00fa1b04d777" + }, + { + "name": "After review", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2860, + 300 + ], + "id": "d07d9992-7e5e-5aab-b925-9387021bbaff" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').first().json.category;\nconst answer = String($('Review').first().json.output ?? '').trim();\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('review');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 1;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['classify', t('After classify')], ['faq', t('After FAQ')], ['ticket', t('After data')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nconst parts = Object.entries(timings).filter(([key]) => key !== 'total_ms');\nresult.slowest_step = parts.sort((a, b) => b[1] - a[1])[0][0].replace(/_ms$/, '');\nconst budget_ms = Number(body.budget_ms ?? 3000);\nresult.llm_ms = ['classify', 'answer', 'review'].reduce((sum, label) => sum + (timings[`${label}_ms`] ?? 0), 0);\nresult.budget_ms = budget_ms;\nresult.over_budget = timings.total_ms > budget_ms;\nreturn [{ json: result }];" + }, + "position": [ + 3080, + 300 + ], + "id": "079e44f0-da3e-57dd-be6a-87025b6a320f" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 3300, + 300 + ], + "id": "8ac7b99c-d9fa-5a1e-a168-33eb7ab5e2a3" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Start clock", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start clock": { + "main": [ + [ + { + "node": "Classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Classify model": { + "ai_languageModel": [ + [ + { + "node": "Classify", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Classify": { + "main": [ + [ + { + "node": "Parse category", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse category": { + "main": [ + [ + { + "node": "After classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "After classify": { + "main": [ + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "After FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "After FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "After data", + "type": "main", + "index": 0 + } + ] + ] + }, + "After data": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "After answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "After answer": { + "main": [ + [ + { + "node": "Review", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review model": { + "ai_languageModel": [ + [ + { + "node": "Review", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Review": { + "main": [ + [ + { + "node": "After review", + "type": "main", + "index": 0 + } + ] + ] + }, + "After review": { + "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/72/answers/72-lab02-challenge-fast.json b/Last3Mon/72/answers/72-lab02-challenge-fast.json new file mode 100644 index 0000000..b72acac --- /dev/null +++ b/Last3Mon/72/answers/72-lab02-challenge-fast.json @@ -0,0 +1,852 @@ +{ + "name": "72 IT helpdesk + one LLM call (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": "4a893fec-ca3a-5349-a7b1-a8de0ac4350c" + }, + { + "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": "da287ac6-5b2c-5d11-a358-2c0d575d61ce" + }, + { + "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": "049a20b6-3c29-5159-a0e6-f5617cf9d8d9" + }, + { + "name": "Start clock", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t: Date.now() } }];" + }, + "position": [ + 440, + 300 + ], + "id": "3b52fb72-af9e-54d9-8928-a1f87966b69f" + }, + { + "name": "Cache key", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const body = $('Question webhook').first().json.body;\nconst key = String(body.question ?? '').trim().toLowerCase().replace(/\\s+/g, ' ');\nreturn [{ json: { ...$json, key, use_cache: body.no_cache !== true } }];" + }, + "position": [ + 660, + 300 + ], + "id": "1aed5faa-1a89-578c-8093-61fe2e9a2555" + }, + { + "name": "Cache lookup", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "position": [ + 880, + 300 + ], + "id": "33b97251-cad7-50a2-805b-ecc7d70001de" + }, + { + "name": "Cached?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.value !== undefined && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 1100, + 300 + ], + "id": "7ad20324-27b3-5f30-9637-a4fa4d977a53" + }, + { + "name": "Cached result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const value = $json.value;\nconst t0 = $('Start clock').first().json.t;\nreturn [{ json: { route: value.route, category: value.category, answer: value.answer, sources: value.sources ?? [],\n llm_calls: 0, cached: true, steps: ['cache'], timings: { total_ms: t0 ? Date.now() - t0 : 0 } } }];" + }, + "position": [ + 1320, + 40 + ], + "id": "b102773a-d6b3-5fd8-9ada-e250f648cc44" + }, + { + "name": "Send cached", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1540, + 40 + ], + "id": "ff6ce5c6-903d-53f6-96f7-a17baabcc1b6" + }, + { + "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": [ + 1320, + 300 + ], + "id": "0e3e780d-eaa1-5c13-a60a-f3112a296820" + }, + { + "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": [ + 1540, + 300 + ], + "id": "f345aacb-db55-5226-bf79-822fd2f814ca" + }, + { + "name": "After data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1760, + 300 + ], + "id": "4c8c6887-4a50-589b-abdf-c8741f1cf589" + }, + { + "name": "Rule classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const question = String($('Question webhook').first().json.body.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nlet category = 'unknown';\nif (/T-?\\d{4}/i.test(question)) category = 'ticket';\nelse if (results.length > 0) category = 'faq';\nreturn [{ json: { category, by: 'rule' } }];" + }, + "position": [ + 1980, + 300 + ], + "id": "b01cd9fc-72fc-5ace-860b-6d607bf1e570" + }, + { + "name": "Known?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category }}", + "rightValue": "unknown", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 2200, + 300 + ], + "id": "2d154b76-eb12-5038-a639-c5608c6bf788" + }, + { + "name": "Classify", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "={{ $('Question webhook').first().json.body.question }}", + "options": { + "systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}", + "maxIterations": 1 + } + }, + "position": [ + 2420, + 300 + ], + "id": "8ba05958-c916-52e4-95ef-0f38f92246c3" + }, + { + "name": "Classify 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": [ + 2420, + 520 + ], + "id": "14555336-1d5b-5860-98cc-cc7febd5fdab" + }, + { + "name": "Parse category", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];" + }, + "position": [ + 2640, + 300 + ], + "id": "53522d0c-94f5-5e97-84ca-dfc99cbea797" + }, + { + "name": "After classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2860, + 300 + ], + "id": "ed776d70-2fb9-5b97-86c8-aa43796e8cb3" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $json.category }}\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.", + "maxIterations": 1 + } + }, + "position": [ + 3080, + 300 + ], + "id": "3cd227a5-1bb5-50e6-9402-69206c7ca546" + }, + { + "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": [ + 3080, + 520 + ], + "id": "f3dad63e-7b9c-5d29-b79e-077754d581b7" + }, + { + "name": "After answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3300, + 300 + ], + "id": "0432c08a-5b0d-5d75-8e06-eab20db15ab1" + }, + { + "name": "Review check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "let answer = String($('Answer').first().json.output ?? '').trim();\nconst problems = [];\nif (!answer) problems.push('empty');\nif (!/[가-힣]/.test(answer)) problems.push('not_korean');\nif (answer.length > 400) { answer = answer.slice(0, 400); problems.push('too_long'); }\nreturn [{ json: { ...$json, answer, review_problems: problems } }];" + }, + "position": [ + 3520, + 300 + ], + "id": "c2d5b0e4-e7e1-5972-8b49-d2aa51694714" + }, + { + "name": "After review", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3740, + 300 + ], + "id": "f031c87a-b44b-55fa-a122-05d93f51fa6e" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').isExecuted ? $('Parse category').first().json.category : $('Rule classify').first().json.category;\nconst answer = $('Review check').first().json.answer;\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('check');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 0;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['data', t('After data')], ['classify', t('After classify')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nreturn [{ json: result }];" + }, + "position": [ + 3960, + 300 + ], + "id": "fc92aa99-281c-5e7a-96fb-6868624c4f34" + }, + { + "name": "Cache it?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category === 'faq' && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 4180, + 300 + ], + "id": "f14260a7-8d0f-5916-a219-c233bc484860" + }, + { + "name": "Store in cache", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($('Cache key').first().json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "method": "POST", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ value: { route: $json.route, category: $json.category, answer: $json.answer, sources: $json.sources }, ttl: 600 }) }}" + }, + "position": [ + 4400, + 180 + ], + "id": "8c73289d-f4b4-50c2-a71e-229d368e7b39" + }, + { + "name": "Result again", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: $('Build result').first().json }];" + }, + "position": [ + 4620, + 180 + ], + "id": "4033230f-4e3a-515a-a980-ed987bc3372e" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 4840, + 300 + ], + "id": "43408f1e-819c-54ef-bb7b-7039f738e6f1" + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Start clock", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start clock": { + "main": [ + [ + { + "node": "Cache key", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache key": { + "main": [ + [ + { + "node": "Cache lookup", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache lookup": { + "main": [ + [ + { + "node": "Cached?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached?": { + "main": [ + [ + { + "node": "Cached result", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached result": { + "main": [ + [ + { + "node": "Send cached", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "After data", + "type": "main", + "index": 0 + } + ] + ] + }, + "After data": { + "main": [ + [ + { + "node": "Rule classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Rule classify": { + "main": [ + [ + { + "node": "Known?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Known?": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Classify model": { + "ai_languageModel": [ + [ + { + "node": "Classify", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Classify": { + "main": [ + [ + { + "node": "Parse category", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse category": { + "main": [ + [ + { + "node": "After classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "After classify": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "After answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "After answer": { + "main": [ + [ + { + "node": "Review check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review check": { + "main": [ + [ + { + "node": "After review", + "type": "main", + "index": 0 + } + ] + ] + }, + "After review": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Cache it?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache it?": { + "main": [ + [ + { + "node": "Store in cache", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Store in cache": { + "main": [ + [ + { + "node": "Result again", + "type": "main", + "index": 0 + } + ] + ] + }, + "Result again": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/72/answers/72-lab02-challenge2-faqcache.json b/Last3Mon/72/answers/72-lab02-challenge2-faqcache.json new file mode 100644 index 0000000..33ff46b --- /dev/null +++ b/Last3Mon/72/answers/72-lab02-challenge2-faqcache.json @@ -0,0 +1,1050 @@ +{ + "name": "72 IT helpdesk + FAQ-id cache (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": "4a893fec-ca3a-5349-a7b1-a8de0ac4350c" + }, + { + "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": "da287ac6-5b2c-5d11-a358-2c0d575d61ce" + }, + { + "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": "049a20b6-3c29-5159-a0e6-f5617cf9d8d9" + }, + { + "name": "Start clock", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t: Date.now() } }];" + }, + "position": [ + 440, + 300 + ], + "id": "3b52fb72-af9e-54d9-8928-a1f87966b69f" + }, + { + "name": "Cache key", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const body = $('Question webhook').first().json.body;\nconst key = String(body.question ?? '').trim().toLowerCase().replace(/\\s+/g, ' ');\nreturn [{ json: { ...$json, key, use_cache: body.no_cache !== true } }];" + }, + "position": [ + 660, + 300 + ], + "id": "1aed5faa-1a89-578c-8093-61fe2e9a2555" + }, + { + "name": "Cache lookup", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "position": [ + 880, + 300 + ], + "id": "33b97251-cad7-50a2-805b-ecc7d70001de" + }, + { + "name": "Cached?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.value !== undefined && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 1100, + 300 + ], + "id": "7ad20324-27b3-5f30-9637-a4fa4d977a53" + }, + { + "name": "Cached result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const value = $json.value;\nconst t0 = $('Start clock').first().json.t;\nreturn [{ json: { route: value.route, category: value.category, answer: value.answer, sources: value.sources ?? [],\n llm_calls: 0, cached: true, cache_by: 'question', steps: ['cache'], timings: { total_ms: t0 ? Date.now() - t0 : 0 } } }];" + }, + "position": [ + 1320, + 40 + ], + "id": "b102773a-d6b3-5fd8-9ada-e250f648cc44" + }, + { + "name": "Send cached", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1540, + 40 + ], + "id": "ff6ce5c6-903d-53f6-96f7-a17baabcc1b6" + }, + { + "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": [ + 1320, + 300 + ], + "id": "0e3e780d-eaa1-5c13-a60a-f3112a296820" + }, + { + "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": [ + 1540, + 300 + ], + "id": "f345aacb-db55-5226-bf79-822fd2f814ca" + }, + { + "name": "After data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1760, + 300 + ], + "id": "4c8c6887-4a50-589b-abdf-c8741f1cf589" + }, + { + "name": "Rule classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const question = String($('Question webhook').first().json.body.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nlet category = 'unknown';\nif (/T-?\\d{4}/i.test(question)) category = 'ticket';\nelse if (results.length > 0) category = 'faq';\nreturn [{ json: { category, by: 'rule' } }];" + }, + "position": [ + 1980, + 300 + ], + "id": "b01cd9fc-72fc-5ace-860b-6d607bf1e570" + }, + { + "name": "Known?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category }}", + "rightValue": "unknown", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 2200, + 300 + ], + "id": "2d154b76-eb12-5038-a639-c5608c6bf788" + }, + { + "name": "Classify", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "={{ $('Question webhook').first().json.body.question }}", + "options": { + "systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}", + "maxIterations": 1 + } + }, + "position": [ + 2420, + 300 + ], + "id": "8ba05958-c916-52e4-95ef-0f38f92246c3" + }, + { + "name": "Classify 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": [ + 2420, + 520 + ], + "id": "14555336-1d5b-5860-98cc-cc7febd5fdab" + }, + { + "name": "Parse category", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];" + }, + "position": [ + 2640, + 300 + ], + "id": "53522d0c-94f5-5e97-84ca-dfc99cbea797" + }, + { + "name": "After classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2860, + 300 + ], + "id": "ed776d70-2fb9-5b97-86c8-aa43796e8cb3" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $json.category }}\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.", + "maxIterations": 1 + } + }, + "position": [ + 3080, + 300 + ], + "id": "3cd227a5-1bb5-50e6-9402-69206c7ca546" + }, + { + "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": [ + 3080, + 520 + ], + "id": "f3dad63e-7b9c-5d29-b79e-077754d581b7" + }, + { + "name": "After answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3300, + 300 + ], + "id": "0432c08a-5b0d-5d75-8e06-eab20db15ab1" + }, + { + "name": "Review check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "let answer = String($('Answer').first().json.output ?? '').trim();\nconst problems = [];\nif (!answer) problems.push('empty');\nif (!/[가-힣]/.test(answer)) problems.push('not_korean');\nif (answer.length > 400) { answer = answer.slice(0, 400); problems.push('too_long'); }\nreturn [{ json: { ...$json, answer, review_problems: problems } }];" + }, + "position": [ + 3520, + 300 + ], + "id": "c2d5b0e4-e7e1-5972-8b49-d2aa51694714" + }, + { + "name": "After review", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3740, + 300 + ], + "id": "f031c87a-b44b-55fa-a122-05d93f51fa6e" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').isExecuted ? $('Parse category').first().json.category : $('Rule classify').first().json.category;\nconst answer = $('Review check').first().json.answer;\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('check');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 0;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['data', t('After data')], ['classify', t('After classify')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, cache_by: null, steps, timings });\nreturn [{ json: result }];" + }, + "position": [ + 3960, + 300 + ], + "id": "fc92aa99-281c-5e7a-96fb-6868624c4f34" + }, + { + "name": "Cache it?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category === 'faq' && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 4180, + 300 + ], + "id": "f14260a7-8d0f-5916-a219-c233bc484860" + }, + { + "name": "Store in cache", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($('Cache key').first().json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "method": "POST", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ value: { route: $json.route, category: $json.category, answer: $json.answer, sources: $json.sources }, ttl: 600 }) }}" + }, + "position": [ + 4400, + 180 + ], + "id": "8c73289d-f4b4-50c2-a71e-229d368e7b39" + }, + { + "name": "Result again", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: $('Build result').first().json }];" + }, + "position": [ + 4620, + 180 + ], + "id": "4033230f-4e3a-515a-a980-ed987bc3372e" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 4840, + 300 + ], + "id": "43408f1e-819c-54ef-bb7b-7039f738e6f1" + }, + { + "name": "FAQ cache key", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2180, + 100 + ], + "parameters": { + "jsCode": "const rule = $('Rule classify').first().json;\nconst results = $('Search FAQ').first().json.results ?? [];\nconst useCache = $('Cache key').first().json.use_cache;\nconst faq_key = rule.category === 'faq' && useCache && results.length > 0 ? `faq-${results[0].id}` : 'none';\nreturn [{ json: { ...rule, faq_key } }];" + } + }, + { + "name": "FAQ cache lookup", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2380, + 100 + ], + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($json.faq_key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + } + }, + { + "name": "FAQ cached?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 2580, + 100 + ], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.value !== undefined && $('FAQ cache key').first().json.faq_key !== 'none' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "name": "FAQ cached result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2780, + -20 + ], + "parameters": { + "jsCode": "const value = $json.value;\nconst t0 = $('Start clock').first().json.t;\nreturn [{ json: { route: value.route, category: value.category, answer: value.answer, sources: value.sources ?? [],\n llm_calls: 0, cached: true, cache_by: 'faq', steps: ['faq', 'rule', 'cache'], timings: { total_ms: t0 ? Date.now() - t0 : 0 } } }];" + } + }, + { + "name": "Send FAQ cached", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 2980, + -20 + ], + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + } + }, + { + "name": "Back to rule", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2780, + 180 + ], + "parameters": { + "jsCode": "return [{ json: $('Rule classify').first().json }];" + } + }, + { + "name": "Store by FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 4600, + 60 + ], + "parameters": { + "method": "POST", + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($('FAQ cache key').first().json.faq_key) }}", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ value: { route: $('Build result').first().json.route, category: $('Build result').first().json.category, answer: $('Build result').first().json.answer, sources: $('Build result').first().json.sources }, ttl: 600 }) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + } + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Start clock", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start clock": { + "main": [ + [ + { + "node": "Cache key", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache key": { + "main": [ + [ + { + "node": "Cache lookup", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache lookup": { + "main": [ + [ + { + "node": "Cached?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached?": { + "main": [ + [ + { + "node": "Cached result", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached result": { + "main": [ + [ + { + "node": "Send cached", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "After data", + "type": "main", + "index": 0 + } + ] + ] + }, + "After data": { + "main": [ + [ + { + "node": "Rule classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Rule classify": { + "main": [ + [ + { + "node": "FAQ cache key", + "type": "main", + "index": 0 + } + ] + ] + }, + "Known?": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Classify model": { + "ai_languageModel": [ + [ + { + "node": "Classify", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Classify": { + "main": [ + [ + { + "node": "Parse category", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse category": { + "main": [ + [ + { + "node": "After classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "After classify": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "After answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "After answer": { + "main": [ + [ + { + "node": "Review check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review check": { + "main": [ + [ + { + "node": "After review", + "type": "main", + "index": 0 + } + ] + ] + }, + "After review": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Cache it?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache it?": { + "main": [ + [ + { + "node": "Store in cache", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Store in cache": { + "main": [ + [ + { + "node": "Store by FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Result again": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "FAQ cache key": { + "main": [ + [ + { + "node": "FAQ cache lookup", + "type": "main", + "index": 0 + } + ] + ] + }, + "FAQ cache lookup": { + "main": [ + [ + { + "node": "FAQ cached?", + "type": "main", + "index": 0 + } + ] + ] + }, + "FAQ cached?": { + "main": [ + [ + { + "node": "FAQ cached result", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Back to rule", + "type": "main", + "index": 0 + } + ] + ] + }, + "FAQ cached result": { + "main": [ + [ + { + "node": "Send FAQ cached", + "type": "main", + "index": 0 + } + ] + ] + }, + "Back to rule": { + "main": [ + [ + { + "node": "Known?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Store by FAQ": { + "main": [ + [ + { + "node": "Result again", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/72/answers/72-lab03-challenge-health.json b/Last3Mon/72/answers/72-lab03-challenge-health.json new file mode 100644 index 0000000..032e971 --- /dev/null +++ b/Last3Mon/72/answers/72-lab03-challenge-health.json @@ -0,0 +1,238 @@ +{ + "name": "72 Health check (answer)", + "nodes": [ + { + "name": "Health webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "parameters": { + "httpMethod": "POST", + "path": "my-health", + "responseMode": "responseNode", + "options": {} + }, + "webhookId": "bdf5dd1e-edb4-503e-b800-c8653ff5fbb4", + "position": [ + 0, + 300 + ], + "id": "cd5b6b9d-06a3-57ed-91ab-0ab44b344fde" + }, + { + "name": "Start", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t0: Date.now() } }];" + }, + "position": [ + 220, + 300 + ], + "id": "0445c332-e249-5cce-915d-fca703b570b8" + }, + { + "name": "Check data API", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/health", + "options": { + "timeout": 2000 + } + }, + "onError": "continueRegularOutput", + "alwaysOutputData": true, + "position": [ + 440, + 300 + ], + "id": "8ce80344-ec8e-5bd5-9a2e-839d49d9f1e9" + }, + { + "name": "Check FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": { + "timeout": 2000 + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "vpn" + } + ] + } + }, + "onError": "continueRegularOutput", + "alwaysOutputData": true, + "position": [ + 660, + 300 + ], + "id": "a935db0a-fc45-5a01-aa52-405740f82e9f" + }, + { + "name": "Check LLM", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "Reply with the single word OK.", + "options": { + "systemMessage": "You are a health check. Reply with OK only.", + "maxIterations": 1 + } + }, + "onError": "continueRegularOutput", + "alwaysOutputData": true, + "position": [ + 880, + 300 + ], + "id": "96a58520-65ae-548a-ac2f-0b41d004933f" + }, + { + "name": "Health model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.2, + "parameters": { + "model": { + "__rl": true, + "mode": "id", + "value": "gpt-4.1-mini" + }, + "options": { + "temperature": 0, + "timeout": 8000, + "maxRetries": 0 + } + }, + "credentials": { + "openAiApi": { + "id": "openAiCourse0001", + "name": "OpenAI (.env)" + } + }, + "position": [ + 880, + 520 + ], + "id": "5aa10812-b13e-5bae-831e-7ccd24e72e62" + }, + { + "name": "Summarize", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const data_api = $('Check data API').first().json.status === 'ok';\nconst faq = Array.isArray($('Check FAQ').first().json.results);\nconst llm = /ok/i.test(String($('Check LLM').first().json.output ?? ''));\nconst checks = { data_api, faq, llm };\nconst failed = Object.entries(checks).filter(([, good]) => !good).map(([name]) => name);\nconst status = failed.length === 0 ? 'ok' : (data_api || llm ? 'degraded' : 'down');\nreturn [{ json: { status, checks, failed, seconds: Math.round((Date.now() - $('Start').first().json.t0) / 100) / 10 } }];" + }, + "position": [ + 1100, + 300 + ], + "id": "54ee87ba-881d-5ad1-a032-1262876fdd48" + }, + { + "name": "Send status", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1320, + 300 + ], + "id": "2340a4f9-d3c9-5717-ad49-b782bcd9f808" + } + ], + "connections": { + "Health model": { + "ai_languageModel": [ + [ + { + "node": "Check LLM", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Health webhook": { + "main": [ + [ + { + "node": "Start", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start": { + "main": [ + [ + { + "node": "Check data API", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check data API": { + "main": [ + [ + { + "node": "Check FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check FAQ": { + "main": [ + [ + { + "node": "Check LLM", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check LLM": { + "main": [ + [ + { + "node": "Summarize", + "type": "main", + "index": 0 + } + ] + ] + }, + "Summarize": { + "main": [ + [ + { + "node": "Send status", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/72/answers/72-lab03-challenge2-breaker.json b/Last3Mon/72/answers/72-lab03-challenge2-breaker.json new file mode 100644 index 0000000..4237eb4 --- /dev/null +++ b/Last3Mon/72/answers/72-lab03-challenge2-breaker.json @@ -0,0 +1,1319 @@ +{ + "name": "72 IT helpdesk + FAQ circuit breaker (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": "da213575-a9a7-5052-9259-1235f4e340f0" + }, + { + "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": "4ea38fda-f28a-5e1d-a504-05874f837f02" + }, + { + "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": "ff743009-a8ea-5c5f-8b83-9e29a2c60111" + }, + { + "name": "Start clock", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { t: Date.now() } }];" + }, + "position": [ + 440, + 300 + ], + "id": "f1cc57e6-9966-5f49-90c4-cbd65be68041" + }, + { + "name": "Cache key", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const body = $('Question webhook').first().json.body;\nconst key = String(body.question ?? '').trim().toLowerCase().replace(/\\s+/g, ' ');\nreturn [{ json: { ...$json, key, use_cache: body.no_cache !== true } }];" + }, + "position": [ + 660, + 300 + ], + "id": "3c70cd68-4a52-59c2-a5c3-d30c86914021" + }, + { + "name": "Cache lookup", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "position": [ + 880, + 300 + ], + "id": "506d8e43-b396-596a-8197-08f2aa58381b" + }, + { + "name": "Cached?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.value !== undefined && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 1100, + 300 + ], + "id": "1113fdf6-f964-5534-8d8a-dd41344c2725" + }, + { + "name": "Cached result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const value = $json.value;\nconst t0 = $('Start clock').first().json.t;\nreturn [{ json: { route: value.route, category: value.category, answer: value.answer, sources: value.sources ?? [],\n llm_calls: 0, cached: true, steps: ['cache'], timings: { total_ms: t0 ? Date.now() - t0 : 0 } } }];" + }, + "position": [ + 1320, + 40 + ], + "id": "82a7f0f0-356b-5164-baaa-a478f74db65e" + }, + { + "name": "Send cached", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 1540, + 40 + ], + "id": "addd6ac5-76a5-5da7-ac2b-84c005eb7520" + }, + { + "name": "Search FAQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "http://data-api:8800/faq", + "options": { + "timeout": 2000 + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "q", + "value": "={{ $('Question webhook').first().json.body.question }}" + } + ] + } + }, + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 500, + "onError": "continueErrorOutput", + "position": [ + 1320, + 300 + ], + "id": "52b6a8a9-bf1f-5b58-8644-0077cfd01dd0" + }, + { + "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": { + "fullResponse": true, + "neverError": true + } + }, + "timeout": 2000 + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "requester", + "value": "={{ $('Question webhook').first().json.body.user ?? '' }}" + } + ] + } + }, + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 500, + "onError": "continueErrorOutput", + "position": [ + 1540, + 300 + ], + "id": "0a16cd98-b769-5067-b867-beabec01b00d" + }, + { + "name": "After data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 1760, + 300 + ], + "id": "ba6f2475-7cea-5c3d-95a6-dc6125a0441c" + }, + { + "name": "Rule classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const question = String($('Question webhook').first().json.body.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nlet category = 'unknown';\nif (/T-?\\d{4}/i.test(question)) category = 'ticket';\nelse if (results.length > 0) category = 'faq';\nreturn [{ json: { category, by: 'rule' } }];" + }, + "position": [ + 1980, + 300 + ], + "id": "a2e2c597-6870-5bcc-833c-a44526b343f3" + }, + { + "name": "Known?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category }}", + "rightValue": "unknown", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 2200, + 300 + ], + "id": "e0758a6f-de55-5309-a696-3ed0755ab1f0" + }, + { + "name": "Classify", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "={{ $('Question webhook').first().json.body.question }}", + "options": { + "systemMessage": "You sort one company IT help-desk question. Categories: faq (how to fix or use something: password, account lock, VPN, printer, mail box, software install), ticket (asks about the status of an IT request ticket like T-1002), other (anything else). Return only JSON, no code fence: {\"category\": \"faq\"}", + "maxIterations": 1 + } + }, + "onError": "continueErrorOutput", + "position": [ + 2420, + 300 + ], + "id": "05ed7c16-110e-53ab-9b03-ce872d616695" + }, + { + "name": "Classify 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": [ + 2420, + 520 + ], + "id": "5a7c30c2-472e-54b3-89c2-85b53e3f432b" + }, + { + "name": "Parse category", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const raw = String($json.output ?? '').trim().replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nif (!['faq', 'ticket', 'other'].includes(category)) category = 'other';\nreturn [{ json: { category, by: 'llm' } }];" + }, + "position": [ + 2640, + 300 + ], + "id": "1407c587-1e10-5d76-b43a-9bf6570eb619" + }, + { + "name": "After classify", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 2860, + 300 + ], + "id": "77f26c5e-fd71-59c5-b02f-cee875e87ee7" + }, + { + "name": "Data down", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "parameters": { + "assignments": { + "assignments": [ + { + "id": "d1", + "name": "route", + "value": "담당자 연결", + "type": "string" + }, + { + "id": "d2", + "name": "answer", + "value": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "type": "string" + }, + { + "id": "d3", + "name": "reason", + "value": "data_api_down", + "type": "string" + } + ] + }, + "options": {} + }, + "position": [ + 3080, + 820 + ], + "id": "7db7288e-627d-5b0f-8634-2bd12ba22e35" + }, + { + "name": "Send data down", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 3300, + 820 + ], + "id": "e582596a-b12d-550c-8ba8-ea5661be5b32" + }, + { + "name": "Answer", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "parameters": { + "promptType": "define", + "text": "=질문: {{ $('Question webhook').first().json.body.question }}\n분류: {{ $json.category }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Ticket check').first().json.ticket) }}", + "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.", + "maxIterations": 1 + } + }, + "onError": "continueErrorOutput", + "position": [ + 3080, + 300 + ], + "id": "a0b37e8a-ba45-5496-baf5-3fe7eb4e11cb" + }, + { + "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": [ + 3080, + 520 + ], + "id": "eadbe0ed-8031-5d85-91c2-258b144dcc6d" + }, + { + "name": "After answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3300, + 300 + ], + "id": "2a0a5546-0677-5a3b-89b9-cfb1887a696b" + }, + { + "name": "Review check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "let answer = String($('Answer').first().json.output ?? '').trim();\nconst problems = [];\nif (!answer) problems.push('empty');\nif (!/[가-힣]/.test(answer)) problems.push('not_korean');\nif (answer.length > 400) { answer = answer.slice(0, 400); problems.push('too_long'); }\nreturn [{ json: { ...$json, answer, review_problems: problems } }];" + }, + "position": [ + 3520, + 300 + ], + "id": "24064ede-b012-51e6-8e8a-a40032982e5b" + }, + { + "name": "After review", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: { ...$json, t: Date.now() } }];" + }, + "position": [ + 3740, + 300 + ], + "id": "d6ea34a2-56fe-5d19-b1fe-a07a2176b6cf" + }, + { + "name": "Build result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const ran = (name) => { try { return $(name).isExecuted; } catch (error) { return false; } };\nconst body = $('Question webhook').first().json.body;\nconst category = $('Parse category').isExecuted ? $('Parse category').first().json.category : $('Rule classify').first().json.category;\nconst answer = $('Review check').first().json.answer;\nconst results = $('Search FAQ').first().json.results ?? [];\nconst sources = category === 'faq' ? results.map((r) => r.id) : [];\nconst steps = [];\nif (ran('Rule classify')) steps.push('rule');\nif (ran('Classify')) steps.push('classify');\nsteps.push('faq', 'ticket', 'answer');\nsteps.push('check');\nconst llm_calls = (ran('Classify') ? 1 : 0) + 1 + 0;\nconst result = { route: '자동 답변', category, answer, sources };\nconst t = (name) => (ran(name) ? $(name).first().json.t : null);\nconst t0 = t('Start clock');\nconst marks = [['data', t('After data')], ['classify', t('After classify')], ['answer', t('After answer')], ['review', t('After review')]];\nconst timings = {};\nlet last = t0;\nfor (const [label, mark] of marks) {\n if (mark === null) continue;\n timings[`${label}_ms`] = mark - last;\n last = mark;\n}\ntimings.total_ms = Date.now() - t0;\nObject.assign(result, { llm_calls, cached: false, steps, timings });\nreturn [{ json: result }];" + }, + "position": [ + 3960, + 300 + ], + "id": "ddb667d7-58f4-591a-99b1-9fdee196e7a9" + }, + { + "name": "Cache it?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.category === 'faq' && $('Cache key').first().json.use_cache }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "position": [ + 4180, + 300 + ], + "id": "be269673-e6ef-5484-adb9-d2b266f59f97" + }, + { + "name": "Store in cache", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "parameters": { + "url": "=http://data-api:8800/cache/{{ encodeURIComponent($('Cache key').first().json.key) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + }, + "method": "POST", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ value: { route: $json.route, category: $json.category, answer: $json.answer, sources: $json.sources }, ttl: 600 }) }}" + }, + "position": [ + 4400, + 180 + ], + "id": "1d551b65-ef7f-527e-8339-ce95e2272e2d" + }, + { + "name": "Result again", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "return [{ json: $('Build result').first().json }];" + }, + "position": [ + 4620, + 180 + ], + "id": "056a1325-c8d0-501f-9dab-599a8380748e" + }, + { + "name": "Send result", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 4840, + 300 + ], + "id": "0bc9ba7d-cc11-5f62-a05e-107089fe53b4" + }, + { + "name": "Simple answer", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "parameters": { + "jsCode": "const body = $('Question webhook').first().json.body;\nlet results = [];\ntry { results = $('Search FAQ').first().json.results ?? []; } catch (error) { results = []; }\nlet ticket = {};\ntry { ticket = $('Ticket check').first().json.ticket ?? {}; } catch (error) { ticket = {}; }\nconst isTicket = /T-?\\d{4}/i.test(String(body.question ?? ''));\nif (isTicket && ticket.status) {\n return [{ json: { route: '간단 답변', category: 'ticket', answer: `${ticket.ticket_id} 접수 건은 현재 '${ticket.status}' 상태입니다. ${ticket.note ?? ''}`.trim(),\n sources: [], llm_calls: 0, reason: 'llm_down' } }];\n}\nif (!isTicket && results.length > 0) {\n return [{ json: { route: '간단 답변', category: 'faq', answer: `${results[0].answer} (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)`,\n sources: [results[0].id], llm_calls: 0, reason: 'llm_down' } }];\n}\nreturn [{ json: { route: '담당자 연결', category: 'other', answer: '지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.', sources: [], llm_calls: 0, reason: 'llm_down' } }];" + }, + "position": [ + 4400, + 1060 + ], + "id": "9d9bceef-74c6-5bea-8e09-8d6fca3dd417" + }, + { + "name": "Send simple answer", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "position": [ + 4620, + 1060 + ], + "id": "3ab6f0cb-6d8f-5292-86c1-86b2645c74b5" + }, + { + "name": "Ticket check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1640, + 460 + ], + "parameters": { + "jsCode": "const question = String($('Question webhook').first().json.body.question ?? '');\nconst status = Number($json.statusCode ?? 200);\nconst is_ticket = /T-?\\d{4}/i.test(question);\nconst tickets_down = status >= 500;\nreturn [{ json: { ticket: tickets_down ? {} : ($json.body ?? {}), ticket_status: status, tickets_down: tickets_down && is_ticket } }];" + } + }, + { + "name": "Tickets down?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1840, + 460 + ], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.tickets_down }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "name": "Tickets down", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 2040, + 620 + ], + "parameters": { + "assignments": { + "assignments": [ + { + "id": "t1", + "name": "route", + "value": "담당자 연결", + "type": "string" + }, + { + "id": "t2", + "name": "category", + "value": "ticket", + "type": "string" + }, + { + "id": "t3", + "name": "answer", + "value": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", + "type": "string" + }, + { + "id": "t4", + "name": "reason", + "value": "tickets_down", + "type": "string" + }, + { + "id": "t5", + "name": "llm_calls", + "value": 0, + "type": "number" + } + ] + }, + "options": {} + } + }, + { + "name": "Send tickets down", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 2240, + 620 + ], + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + } + }, + { + "name": "Breaker check", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1160, + 100 + ], + "parameters": { + "url": "http://data-api:8800/cache/breaker-faq", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + } + }, + { + "name": "Breaker open?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1320, + 100 + ], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "c1", + "leftValue": "={{ $json.value !== undefined }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "name": "Breaker open", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1520, + -60 + ], + "parameters": { + "assignments": { + "assignments": [ + { + "id": "b1", + "name": "route", + "value": "담당자 연결", + "type": "string" + }, + { + "id": "b2", + "name": "answer", + "value": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "type": "string" + }, + { + "id": "b3", + "name": "reason", + "value": "breaker_open", + "type": "string" + }, + { + "id": "b4", + "name": "llm_calls", + "value": 0, + "type": "number" + } + ] + }, + "options": {} + } + }, + { + "name": "Send breaker open", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 1720, + -60 + ], + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + } + }, + { + "name": "Open breaker", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1520, + 660 + ], + "parameters": { + "method": "POST", + "url": "http://data-api:8800/cache/breaker-faq", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ value: { open: true, since: Date.now() }, ttl: 30 }) }}", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + } + } + ], + "connections": { + "Question webhook": { + "main": [ + [ + { + "node": "Has question?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has question?": { + "main": [ + [ + { + "node": "Start clock", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject empty", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start clock": { + "main": [ + [ + { + "node": "Cache key", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache key": { + "main": [ + [ + { + "node": "Cache lookup", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache lookup": { + "main": [ + [ + { + "node": "Cached?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached?": { + "main": [ + [ + { + "node": "Cached result", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Breaker check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cached result": { + "main": [ + [ + { + "node": "Send cached", + "type": "main", + "index": 0 + } + ] + ] + }, + "Search FAQ": { + "main": [ + [ + { + "node": "Get ticket", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Open breaker", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get ticket": { + "main": [ + [ + { + "node": "Ticket check", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Data down", + "type": "main", + "index": 0 + } + ] + ] + }, + "After data": { + "main": [ + [ + { + "node": "Rule classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Rule classify": { + "main": [ + [ + { + "node": "Known?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Known?": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Classify model": { + "ai_languageModel": [ + [ + { + "node": "Classify", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Classify": { + "main": [ + [ + { + "node": "Parse category", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Simple answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse category": { + "main": [ + [ + { + "node": "After classify", + "type": "main", + "index": 0 + } + ] + ] + }, + "Data down": { + "main": [ + [ + { + "node": "Send data down", + "type": "main", + "index": 0 + } + ] + ] + }, + "After classify": { + "main": [ + [ + { + "node": "Answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer model": { + "ai_languageModel": [ + [ + { + "node": "Answer", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Answer": { + "main": [ + [ + { + "node": "After answer", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Simple answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "After answer": { + "main": [ + [ + { + "node": "Review check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Review check": { + "main": [ + [ + { + "node": "After review", + "type": "main", + "index": 0 + } + ] + ] + }, + "After review": { + "main": [ + [ + { + "node": "Build result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build result": { + "main": [ + [ + { + "node": "Cache it?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cache it?": { + "main": [ + [ + { + "node": "Store in cache", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Store in cache": { + "main": [ + [ + { + "node": "Result again", + "type": "main", + "index": 0 + } + ] + ] + }, + "Result again": { + "main": [ + [ + { + "node": "Send result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Simple answer": { + "main": [ + [ + { + "node": "Send simple answer", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ticket check": { + "main": [ + [ + { + "node": "Tickets down?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tickets down?": { + "main": [ + [ + { + "node": "Tickets down", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "After data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tickets down": { + "main": [ + [ + { + "node": "Send tickets down", + "type": "main", + "index": 0 + } + ] + ] + }, + "Breaker check": { + "main": [ + [ + { + "node": "Breaker open?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Breaker open?": { + "main": [ + [ + { + "node": "Breaker open", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Search FAQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Breaker open": { + "main": [ + [ + { + "node": "Send breaker open", + "type": "main", + "index": 0 + } + ] + ] + }, + "Open breaker": { + "main": [ + [ + { + "node": "Data down", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +} diff --git a/Last3Mon/72/answers/LAB-TEST-RESULT.md b/Last3Mon/72/answers/LAB-TEST-RESULT.md new file mode 100644 index 0000000..008fa3f --- /dev/null +++ b/Last3Mon/72/answers/LAB-TEST-RESULT.md @@ -0,0 +1,6407 @@ +# 72일차 Lab 테스트 결과 · 실제로 만든 시나리오와 `test -v` 결과 + +> **정답 자료** · 72일차 n8n 워크플로우를 **만드는 프롬프트(codex) → push → `call -v` · `bench` · `test -v` 결과**를 학생이 가는 순서대로 한 문서에 모았습니다. 수업이 끝난 뒤 공개합니다. + +- 실행: 2026-10-24, 실습을 Lab마다 5개(예제 3 + 과제 2)로 늘린 뒤 강사 노트북의 65일차 n8n 2.42.6 + 72일차 판 데이터 API(고장 스위치 · 캐시), OpenAI `gpt-4.1-mini`, 케이스 파일마다 **한 번씩** (Lab 02 과제 1만 `--repeat 3`), 모두 45단계 +- 명령: `~/aio-01d4-stu/Last3Mon/72`에서 `uv run tools/flow.py test <케이스 파일> -v`, 한 번 부르기는 `call ... -v`, 시간 재기는 `bench ... -n 5` +- 아래 단계마다 **실행한 명령**과 **화면에 나온 출력 전부**를 그대로 붙였습니다. 같은 출력이 `NN-<이름>.txt`에, `test`의 보고서가 `NN-<이름>.json`(케이스마다 `request` · `response` · `problems` · `seconds`)에 있습니다 (`call` · `bench` · `fault` · `llm` · `doctor`만 한 단계는 보고서 없음). +- LLM 문장과 **시간은 실행마다 다릅니다.** 이번 실행에서 정답이 아닌 이유로 실패한 케이스는 셋이고 모두 같은 까닭입니다: **검토 LLM이 접수 답의 "처리 중"을 다른 말("승인 대기 중" 등)로 바꿔 씀** (02 · 12 · 15단계). 검토 LLM을 지우는 Lab 02 과제 1 뒤로는 나오지 않습니다. + +## `-v`가 보여 주는 것 + +```text +✔ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 0.1s ← 케이스 이름 · 시간 (틀리면 틀린 곳) + → request POST /webhook/my-helpdesk ← 보낸 요청 JSON + { "question": "VPN 연결이 계속 끊어지는데요", "user": "최유진" } + ← response HTTP 200 ← 받은 응답 JSON + { "route": "자동 답변", ..., "cached": true, "cache_by": "faq", "llm_calls": 0 } +``` + +오늘은 응답의 `timings` · `steps` · `llm_calls` · `cached` · `cache_by` · `reason`을 봐야 **왜 빨라졌는지, 어느 길로 갔는지** 알 수 있습니다. + +## 이 문서 하나로 따라가기 + +단계마다 **① codex에 넣는 프롬프트 → ② codex가 `my-work/*.json`을 쓰고 `check` → ③ 내가 `push` → ④ 내가 `test -v` · `bench`** 순서입니다. 재기만 하는 예제(Lab 01 예제 1 · 3, Lab 02 예제 3)와 고장을 먼저 겪는 단계는 프롬프트가 없습니다. + +```mermaid +flowchart LR + P["① 프롬프트
터미널 ① codex"] --> C["② codex
my-work/*.json · check"] --> U["③ push
터미널 ②"] --> T["④ test -v · bench
요청 · 응답 · 시간"] + T -->|✘| X["실패 줄 · 보고서 경로를
codex에 그대로"] --> C +``` + +- **프롬프트는 정답본(`LAB-0x-A-*.md`)의 요청문 예**를 그대로 썼습니다 (실습 문서 빈칸 채우기 뼈대의 `____`를 채운 글자). +- **이 기록의 워크플로우는 수업 준비 때 만든 정답 파일**입니다. 프롬프트를 codex에 보내 다시 만들어 재지는 않았습니다 (68일차부터 수업 준비는 정답 워크플로우만 실행). +- 학생은 `push my-work/helpdesk.json`, 이 기록은 같은 웹훅(`my-helpdesk`)의 정답 파일을 push했습니다. 그래서 `test` 명령과 결과 모양은 같습니다. +- 시간을 보는 케이스(`max_seconds`)는 노트북 · 네트워크 · OpenAI 응답에 따라 흔들립니다. 실패하면 먼저 같은 테스트를 한 번 더 돌려 봅니다. + +## 시나리오 한눈에 + +```mermaid +flowchart LR + L1["Lab 01 (00~11)
기준 p50 2.69초 · LLM 98%
과제 1 0/4 → 4/4
과제 2 0/4 → 4/4"] --> L2["Lab 02 (12~27)
캐시 0.05초
과제 1 1/5 → 15/15 · p50 2.22 → 1.12초
과제 2 4/6 → 6/6"] + L2 --> L3["Lab 03 (28~44)
예제 1 · 2 · 3 모두 고치기 전 → 4/4
과제 1 4/4 · 과제 2 3/4 → 4/4
doctor ready"] +``` + +| 단계 | 먼저 올린 것 (`push`) | 테스트 · 재기 | 결과 | 파일 | +| --- | --- | --- | --- | --- | +| 00 | 기준 `reference/baseline-helpdesk.json` | `call` + `bench` × 3 | 첫 호출 4.4초 · VPN p50 2.69 / p95 2.98초 | `00-lab01-baseline` | +| 01 | (같음) | `lab01 --webhook base-helpdesk` | **1/4** (시간 칸 없음) | `01-lab01-baseline` | +| 02 | 예제 2 `reference/lab01-example2-timings.json` | `lab01` | 3/4 (접수 답에서 검토 LLM이 "처리 중"을 바꿈) | `02-lab01` | +| 03 | (같음) | `call` VPN | `timings` · `llm_calls: 3` | `03-lab01-call` | +| 04 | (같음) | 예제 3 `bench` × 3 + `call` × 2 | VPN 2.91 · 접수 2.99 · 점심 2.48초 (p50), LLM 98% | `04-lab01-kinds` | +| 05 | (같음, 과제 1 전) | `lab01-challenge` | **0/4** | `05-lab01-challenge-before` | +| 06 | 과제 1 [`72-lab01-challenge-timings.json`](72-lab01-challenge-timings.json) | `lab01-challenge` | 4/4 | `06-lab01-challenge` | +| 07 | (같음) | `lab01` | 4/4 | `07-lab01-after-challenge` | +| 08 | (같음, 과제 2 전) | `lab01-challenge2` | **0/4** | `08-lab01-challenge2-before` | +| 09 | 과제 2 [`72-lab01-challenge2-budget.json`](72-lab01-challenge2-budget.json) | `lab01-challenge2` | 4/4 | `09-lab01-challenge2` | +| 10 | (같음) | `lab01-challenge` | 4/4 | `10-lab01-challenge-after2` | +| 11 | (같음) | `lab01` | 4/4 | `11-lab01-after2` | +| 12 | 예제 1 `reference/lab02-example1-rules.json` | `lab02` | 7/9 (캐시 케이스 · 접수 답 문장) | `12-lab02-rules` | +| 13 | (같음) | `bench` 프린터 | p50 2.45초 | `13-lab02-rules-bench` | +| 14 | 예제 2 `reference/lab02-example2-cache.json` | `lab02` | 9/9 (같은 질문 0.1초) | `14-lab02-cache` | +| 15 | (같음) | `lab01` | 3/4 (접수 답 문장) | `15-lab01-cache` | +| 16 | (같음) | `cache-stats` | entries 2 · hits 4 · misses 8 | `16-cache-stats` | +| 17 | (같음) | 예제 3 캐시 씀 · 안 씀 `bench` | p50 **0.05** · 1.99초 | `17-cache-hit-bench` | +| 18 | (같음, 과제 1 전) | `lab02-challenge` | **1/5** | `18-lab02-challenge-before` | +| 19 | (같음) | `bench` VPN 캐시 없이 | p50 2.22 / p95 2.45초 | `19-bench-before` | +| 20 | 과제 1 [`72-lab02-challenge-fast.json`](72-lab02-challenge-fast.json) | `lab02-challenge --repeat 3` | 15/15 | `20-lab02-challenge` | +| 21 | (같음) | `lab02` | 9/9 | `21-lab02-after-challenge` | +| 22 | (같음) | `lab01` | 4/4 | `22-lab01-after-challenge` | +| 23 | (같음) | `bench` VPN 캐시 없이 | p50 **1.12** / p95 1.32초 | `23-bench-after` | +| 24 | (같음, 과제 2 전) | `lab02-challenge2` | **4/6** | `24-lab02-challenge2-before` | +| 25 | 과제 2 [`72-lab02-challenge2-faqcache.json`](72-lab02-challenge2-faqcache.json) | `lab02-challenge2` | 6/6 (다른 말 VPN 0.1초) | `25-lab02-challenge2` | +| 26 | (같음) | `lab02-challenge` | 5/5 | `26-lab02-challenge-after2` | +| 27 | (같음) | `lab02` | 9/9 | `27-lab02-after2` | +| 28 | Lab 02 예제 2 다시 | `fault faq slow · error` + `call` | 느림 11.5초 · 오류 HTTP 500 | `28-lab03-fault-before` | +| 29 | 예제 1 `reference/lab03-example1-resilient.json` | `lab03` | 4/4 (느림 4.6초 · 오류 0.6초) | `29-lab03` | +| 30 | (같음) | `lab02` | 9/9 | `30-lab02-resilient` | +| 31 | (같음, 예제 2 전) | `llm down` + `call` + `llm up` | HTTP 500 · 1.3초 | `31-llm-down-before` | +| 32 | (같음) | `lab03-llm-down` | **1/4** | `32-lab03-llm-down-before` | +| 33 | 예제 2 `reference/lab03-example2-llm-fallback.json` | `lab03-llm-down` | 4/4 | `33-lab03-llm-down` | +| 34 | (같음) | `lab02` | 9/9 | `34-lab02-llm-fallback` | +| 35 | (같음) | `lab03` | 4/4 | `35-lab03-llm-fallback` | +| 36 | (같음, 예제 3 전) | `lab03-ticket` | **2/4** | `36-lab03-ticket-before` | +| 37 | 예제 3 `reference/lab03-example3-ticket-down.json` | `lab03-ticket` | 4/4 | `37-lab03-ticket` | +| 38 | (같음) | `lab03` | 4/4 | `38-lab03-after-ticket` | +| 39 | (같음) | `lab03-llm-down` | 4/4 | `39-lab03-llm-down-after-ticket` | +| 40 | 과제 1 [`72-lab03-challenge-health.json`](72-lab03-challenge-health.json) | `lab03-challenge` | 4/4 | `40-lab03-challenge` | +| 41 | (헬프데스크는 예제 3, 과제 2 전) | `lab03-challenge2` | **3/4** | `41-lab03-challenge2-before` | +| 42 | 과제 2 [`72-lab03-challenge2-breaker.json`](72-lab03-challenge2-breaker.json) | `lab03-challenge2` | 4/4 (다음 질문 0.1초) | `42-lab03-challenge2` | +| 43 | (같음) | `lab03` | 4/4 | `43-lab03-after-breaker` | +| 44 | — | `doctor` | `ready` | `44-doctor` | + +--- + +## Lab 01 · 재고, 단계별 시간을 남기고, 병목 찾기 + +### 00 · 예제 1 · 기준을 bench로 재기 + +실행한 명령: + +```bash +uv run tools/flow.py push reference/baseline-helpdesk.json +uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v +uv run tools/flow.py bench base-helpdesk requests/faq-vpn.json -n 5 +uv run tools/flow.py bench base-helpdesk requests/ticket-own.json -n 5 +uv run tools/flow.py bench base-helpdesk requests/off-topic.json -n 5 +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v +HTTP 200 · 4.4s + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ] + } + +$ uv run tools/flow.py bench base-helpdesk requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 2.98s · route=자동 답변 +✔ #2 · HTTP 200 · 2.52s · route=자동 답변 +✔ #3 · HTTP 200 · 2.69s · route=자동 답변 +✔ #4 · HTTP 200 · 2.84s · route=자동 답변 +✔ #5 · HTTP 200 · 2.54s · route=자동 답변 +p50 2.69s · p95 2.98s · max 2.98s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174234.json + +$ uv run tools/flow.py bench base-helpdesk requests/ticket-own.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 3.04s · route=자동 답변 +✔ #2 · HTTP 200 · 3.14s · route=자동 답변 +✔ #3 · HTTP 200 · 2.61s · route=자동 답변 +✔ #4 · HTTP 200 · 3.36s · route=자동 답변 +✔ #5 · HTTP 200 · 2.68s · route=자동 답변 +p50 3.04s · p95 3.36s · max 3.36s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174249.json + +$ uv run tools/flow.py bench base-helpdesk requests/off-topic.json -n 5 +POST http://localhost:5678/webhook/base-helpdesk × 5 +✔ #1 · HTTP 200 · 2.90s · route=자동 답변 +✔ #2 · HTTP 200 · 2.55s · route=자동 답변 +✔ #3 · HTTP 200 · 2.62s · route=자동 답변 +✔ #4 · HTTP 200 · 2.56s · route=자동 답변 +✔ #5 · HTTP 200 · 2.44s · route=자동 답변 +p50 2.56s · p95 2.90s · max 2.90s · failures 0/5 +report: runs/bench-base-helpdesk-20261024-174302.json +``` + +**쉽게 말하면** + +- `call`은 한 번, `bench`는 같은 질문을 5번 불러 시간을 잽니다. 모두 ✔(HTTP 200)라 실패는 없습니다. +- 첫 `call`이 4.4초로 유난히 느린 것은 push로 n8n이 막 다시 켜졌기 때문입니다 (첫 호출, 콜드 스타트). 그래서 기준 숫자는 그 뒤 `bench`의 p50 · p95로 적습니다. +- 질문 세 종류 모두 2.5~3초 안팎입니다. IT와 상관없는 점심 질문도 거의 같은 시간이 걸린다는 것이 눈여겨볼 점입니다 (LLM을 세 번 똑같이 부르기 때문). + +- push 직후 첫 호출 4.4초 (10/18 6.5초), 그 뒤 VPN p50 2.69 · p95 2.98초, 접수 3.04 · 3.36초, 점심 2.56 · 2.90초. + +### 01 · 기준으로 `lab01` → 1/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v --webhook base-helpdesk +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/base-helpdesk · 4 case(s) × 1 +✘ VPN FAQ · 단계별 시간 · 3.5s · timings has none of ['answer_ms'] + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } +✘ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.9s · timings has none of ['total_ms']; steps has none of ['answer']; llm_calls has none of ['1', '2', '3'] + → request POST /webhook/base-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ] + } +✘ 접수 현황 · 2.8s · timings has none of ['answer_ms'] + → request POST /webhook/base-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [] + } +✔ 빈 질문 · 0.0s + → request POST /webhook/base-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/4 +report: runs/lab01-20261024-174311.json +``` + +**쉽게 말하면** + +- 기준 워크플로우는 **아직 아무것도 고치지 않은** 것이라 실패가 정상입니다. +- ✘ `timings has none of ['answer_ms']`: 응답에 단계별 시간 칸 `timings`가 아예 없어서 `answer_ms`를 찾지 못했다는 뜻입니다. +- ✘ `steps has none of ['answer']; llm_calls has none of ['1', '2', '3']`: 지나간 단계(`steps`)와 LLM 호출 수(`llm_calls`)도 없다는 뜻입니다. 예제 2에서 이 칸들을 만듭니다. +- ✔ 빈 질문은 처음부터 HTTP 400으로 거절하도록 되어 있어 통과합니다. + +- 기준에는 `timings` · `steps` · `llm_calls`가 없어 빈 질문만 통과. + +### 02 · 예제 2 · 단계별 시간 → `lab01` 3/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json` · 이 기록에서 push한 같은 역할의 정답 `reference/lab01-example2-timings.json`): + +```text +my-work/helpdesk.json 은 reference/baseline-helpdesk.json 을 복사한 것이야. 웹훅 경로를 my-helpdesk, 이름을 "72 IT helpdesk (my)"로 바꾸고, +답은 그대로 두고 결과에 다음을 더해 줘 (AGENTS.md 3절, 5절의 시각 남기기). +- Has question? 다음에 Start clock, Parse category 뒤에 After classify, Get ticket 뒤에 After data, Answer 뒤에 After answer, Review 뒤에 After review (각각 Date.now()). +- Build result 에서 timings {classify_ms, data_ms, answer_ms, review_ms, total_ms}, steps(지나간 단계), llm_calls(지나간 Agent 수), cached:false. + 지나갔는지는 $('노드').isExecuted 로. +기대 결과 cases/lab01.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab01-example2-timings.json +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 4.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1210, + "data_ms": 29, + "answer_ms": 1119, + "review_ms": 1042, + "total_ms": 3426 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 753, + "data_ms": 19, + "answer_ms": 1058, + "review_ms": 890, + "total_ms": 2731 + } + } +✘ 접수 현황 · 3.3s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 929, + "data_ms": 21, + "answer_ms": 1118, + "review_ms": 1029, + "total_ms": 3108 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/4 +report: runs/lab01-20261024-174333.json +``` + +**쉽게 말하면** + +- `timings` · `steps` · `llm_calls`를 넣은 뒤라 시간 · 구조 케이스는 ✔입니다. +- ✘ `접수 현황 · answer has none of ['처리 중']`: 답에 접수 상태 "처리 중"이라는 글자가 없었다는 뜻입니다. 워크플로우 잘못이 아니라 **검토 LLM이 문장을 다듬다가 "처리 중"을 다른 말로 바꿔 쓴 것**입니다 (LLM 문장은 실행마다 달라짐). +- 그래서 같은 테스트를 다시 돌리면 통과하기도 합니다 (07단계는 4/4). 이 문제는 Lab 02 과제 1에서 검토 LLM을 지우면 사라집니다. + +- 접수 케이스는 검토 LLM이 "처리 중"을 바꿔 써서 실패 (정답의 잘못이 아님, 10/18 · 10/23은 4/4). + +### 03 · 예제 2 · 한 번 불러 `timings` 보기 + +실행한 명령: + +```bash +uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn.json -v +HTTP 200 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하시고, 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 616, + "data_ms": 18, + "answer_ms": 1124, + "review_ms": 920, + "total_ms": 2698 + } + } +``` + +**쉽게 말하면** + +- 응답의 `timings`를 보면 시간이 어디서 쓰였는지 보입니다: 분류 0.6초 + 데이터 0.02초 + 답 1.1초 + 검토 0.9초. +- 데이터 API는 0.02초뿐이고 나머지는 모두 LLM입니다. **느린 곳은 LLM 세 번**이라는 것이 오늘 고칠 방향입니다. + +- 분류 616 · 데이터 18 · 답 1124 · 검토 920 · 합 2698ms. LLM 셋이 98.6%. + +### 04 · 예제 3 · 질문 종류별로 재기 + +실행한 명령: + +```bash +uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +uv run tools/flow.py bench my-helpdesk requests/ticket-own.json -n 5 +uv run tools/flow.py bench my-helpdesk requests/off-topic.json -n 5 +uv run tools/flow.py call my-helpdesk requests/ticket-own.json -v +uv run tools/flow.py call my-helpdesk requests/off-topic.json -v +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.57s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 3.08s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 2.91s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.80s · route=자동 답변, llm_calls=3, cached=false +p50 2.91s · p95 3.08s · max 3.08s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174351.json + +$ uv run tools/flow.py bench my-helpdesk requests/ticket-own.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.86s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 3.05s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 3.12s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.94s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.99s · route=자동 답변, llm_calls=3, cached=false +p50 2.99s · p95 3.12s · max 3.12s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174406.json + +$ uv run tools/flow.py bench my-helpdesk requests/off-topic.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.38s · route=자동 답변, llm_calls=3, cached=false +✔ #2 · HTTP 200 · 2.36s · route=자동 답변, llm_calls=3, cached=false +✔ #3 · HTTP 200 · 2.48s · route=자동 답변, llm_calls=3, cached=false +✔ #4 · HTTP 200 · 2.49s · route=자동 답변, llm_calls=3, cached=false +✔ #5 · HTTP 200 · 2.84s · route=자동 답변, llm_calls=3, cached=false +p50 2.48s · p95 2.84s · max 2.84s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174419.json + +$ uv run tools/flow.py call my-helpdesk requests/ticket-own.json -v +HTTP 200 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 818, + "data_ms": 43, + "answer_ms": 877, + "review_ms": 895, + "total_ms": 2647 + } + } + +$ uv run tools/flow.py call my-helpdesk requests/off-topic.json -v +HTTP 200 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "other", + "answer": "찾으시는 정보를 확인하지 못했습니다. 점심 메뉴 추천은 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 732, + "data_ms": 19, + "answer_ms": 740, + "review_ms": 978, + "total_ms": 2482 + } + } +``` + +**쉽게 말하면** + +- 질문 종류마다 5번씩 잽니다. 결과 끝의 `llm_calls=3`은 세 종류 모두 LLM을 세 번 불렀다는 뜻입니다. +- 점심 메뉴처럼 데이터가 없는 질문도 LLM 세 번 → 2.5초입니다. "건너뛸 수 있는 일을 하고 있다"는 증거입니다 (Lab 02에서 줄임). + +- p50: VPN 2.91 · 접수 2.99 · 점심 2.48초. 접수 2647ms 중 LLM 2590ms(97.8%), 점심 2482ms 중 2450ms(98.7%). +- 점심 메뉴는 데이터가 없는데도 LLM 세 번 (`category: other`). + +### 05 · 과제 1 하기 전 → `lab01-challenge` 0/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✘ FAQ · 접수 시간 따로 · 2.7s · timings has none of ['faq_ms'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 595, + "data_ms": 18, + "answer_ms": 1005, + "review_ms": 1012, + "total_ms": 2643 + } + } +✘ 접수 조회 시간 · 2.6s · timings has none of ['ticket_ms'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 716, + "data_ms": 19, + "answer_ms": 958, + "review_ms": 881, + "total_ms": 2594 + } + } +✘ 가장 오래 걸린 단계 · 3.1s · slowest_step has none of ['classify', 'answer', 'review'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 991, + "data_ms": 18, + "answer_ms": 1024, + "review_ms": 991, + "total_ms": 3037 + } + } +✘ 접수 현황도 같은 모양 · 2.8s · answer has none of ['처리 중']; slowest_step has none of ['classify', 'answer', 'review'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 803, + "data_ms": 36, + "answer_ms": 884, + "review_ms": 1043, + "total_ms": 2781 + } + } +Passed 0/4 +report: runs/lab01-challenge-20261024-174435.json +``` + +**쉽게 말하면** + +- 과제 1을 하기 **전**이라 0/4가 정상입니다. 실패 줄은 "무엇을 만들어야 하는지" 알려 주는 목록으로 읽습니다. +- ✘ `timings has none of ['faq_ms']` · `['ticket_ms']`: 데이터 조회 시간이 아직 `data_ms` 하나로 합쳐져 있어 FAQ · 접수 칸이 따로 없습니다. +- ✘ `slowest_step has none of [...]`: 가장 느린 단계 이름 칸 `slowest_step`이 아직 없습니다. +- 마지막 케이스의 `answer has none of ['처리 중']`은 02단계와 같은 까닭(검토 LLM이 바꿔 씀)입니다. + +- `faq_ms` · `ticket_ms` · `slowest_step`이 없음. + +### 06 · 과제 1 · FAQ · 접수 나누기와 `slowest_step` → 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab01-challenge-timings.json`](72-lab01-challenge-timings.json)): + +```text +my-work/helpdesk.json 의 Search FAQ 와 Get ticket 사이에 After FAQ(Date.now()) 를 더해, timings 의 data_ms 를 faq_ms 와 ticket_ms 로 나눠 줘. +total_ms 를 뺀 timings 중 가장 큰 단계 이름(_ms 뺀 것)을 slowest_step 으로. cases/lab01-challenge.json, cases/lab01.json 둘 다 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab01-challenge-timings.json +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ FAQ · 접수 시간 따로 · 4.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1365, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1196, + "review_ms": 993, + "total_ms": 3614 + }, + "slowest_step": "classify" + } +✔ 접수 조회 시간 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 774, + "faq_ms": 17, + "ticket_ms": 13, + "answer_ms": 971, + "review_ms": 815, + "total_ms": 2601 + }, + "slowest_step": "answer" + } +✔ 가장 오래 걸린 단계 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 680, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1044, + "review_ms": 1007, + "total_ms": 2769 + }, + "slowest_step": "answer" + } +✔ 접수 현황도 같은 모양 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 723, + "faq_ms": 13, + "ticket_ms": 11, + "answer_ms": 1029, + "review_ms": 1162, + "total_ms": 2951 + }, + "slowest_step": "review" + } +Passed 4/4 +report: runs/lab01-challenge-20261024-174502.json +``` + +**쉽게 말하면** + +- 칸을 나누고 `slowest_step`을 더한 뒤 4/4입니다. +- `slowest_step`은 실행마다 바뀝니다: push 직후 첫 호출은 분류(`classify`)가 가장 느렸고, 그 뒤로는 답 · 검토가 번갈아 1등입니다. 답 LLM과 검토 LLM의 시간이 비슷하기 때문입니다. 그래서 테스트는 셋 중 하나면 통과로 봅니다. + +- `slowest_step`: classify 1 (push 직후 첫 호출, classify 1365ms) · answer 2 · review 1. 실행마다 바뀜. + +### 07 · 과제 1 뒤 `lab01` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 844, + "faq_ms": 16, + "ticket_ms": 9, + "answer_ms": 990, + "review_ms": 927, + "total_ms": 2807 + }, + "slowest_step": "answer" + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.8s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 715, + "faq_ms": 16, + "ticket_ms": 14, + "answer_ms": 1064, + "review_ms": 988, + "total_ms": 2810 + }, + "slowest_step": "answer" + } +✔ 접수 현황 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 810, + "faq_ms": 16, + "ticket_ms": 11, + "answer_ms": 1014, + "review_ms": 1058, + "total_ms": 2925 + }, + "slowest_step": "review" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174511.json +``` + +**쉽게 말하면** + +- 과제 1에서 칸을 더했어도 원래 테스트(`lab01`)가 그대로 통과하는지 확인합니다. 4/4라 **원래 계약을 깨지 않았습니다.** +- 이번에는 검토 LLM이 "처리 중"을 그대로 두어 02단계에서 실패했던 접수 케이스도 통과했습니다. + +- 이번에는 접수 답에 "처리 중"이 남아 통과. + +### 08 · 과제 2 하기 전 → `lab01-challenge2` 0/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 +✘ 예산 안 (넉넉한 예산) · 2.9s · over_budget = null (expected false); budget_ms = null (expected 60000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 628, + "faq_ms": 11, + "ticket_ms": 11, + "answer_ms": 1142, + "review_ms": 1028, + "total_ms": 2862 + }, + "slowest_step": "answer" + } +✘ 예산 넘음 (0.5초 예산) · 2.7s · over_budget = null (expected true); budget_ms = null (expected 500) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 658, + "faq_ms": 14, + "ticket_ms": 11, + "answer_ms": 1013, + "review_ms": 930, + "total_ms": 2640 + }, + "slowest_step": "answer" + } +✘ 예산을 안 주면 3초 · 3.6s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 650, + "faq_ms": 14, + "ticket_ms": 13, + "answer_ms": 1767, + "review_ms": 1061, + "total_ms": 3520 + }, + "slowest_step": "answer" + } +✘ 접수 현황도 같은 모양 · 2.8s · budget_ms = null (expected 3000); llm_ms has none of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 772, + "faq_ms": 14, + "ticket_ms": 12, + "answer_ms": 895, + "review_ms": 1006, + "total_ms": 2719 + }, + "slowest_step": "review" + } +Passed 0/4 +report: runs/lab01-challenge2-20261024-174523.json +``` + +**쉽게 말하면** + +- 과제 2를 하기 **전**이라 0/4가 정상입니다. +- ✘ `over_budget = null (expected false)` · `budget_ms = null (expected 60000)`: 응답에 그 칸이 없어서 값이 `null`(없음)로 읽혔다는 뜻입니다. +- ✘ `llm_ms has none of ['0', ... '9']`: `llm_ms`에 숫자가 하나도 없다, 곧 칸이 없다는 뜻입니다. + +- `budget_ms` · `over_budget` · `llm_ms`가 없음. + +### 09 · 과제 2 · 시간 예산 → `lab01-challenge2` 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab01-challenge2-budget.json`](72-lab01-challenge2-budget.json)): + +```text +my-work/helpdesk.json 의 Build result 에서, 본문 budget_ms (없으면 3000, 숫자로)를 읽어 +결과에 llm_ms(classify · answer · review 의 _ms 합, timings 밖에 둔다), budget_ms, over_budget(timings.total_ms > budget_ms)을 더해 줘. +timings, slowest_step 은 그대로. cases/lab01-challenge2.json, cases/lab01-challenge.json, cases/lab01.json 모두 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab01-challenge2-budget.json +uv run tools/flow.py test cases/lab01-challenge2.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 예산 안 (넉넉한 예산) · 4.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 60000 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 1208, + "faq_ms": 17, + "ticket_ms": 14, + "answer_ms": 1230, + "review_ms": 857, + "total_ms": 3357 + }, + "slowest_step": "answer", + "llm_ms": 3295, + "budget_ms": 60000, + "over_budget": false + } +✔ 예산 넘음 (0.5초 예산) · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true, + "budget_ms": 500 + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 667, + "faq_ms": 15, + "ticket_ms": 11, + "answer_ms": 980, + "review_ms": 916, + "total_ms": 2609 + }, + "slowest_step": "answer", + "llm_ms": 2563, + "budget_ms": 500, + "over_budget": true + } +✔ 예산을 안 주면 3초 · 3.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 761, + "faq_ms": 18, + "ticket_ms": 18, + "answer_ms": 1027, + "review_ms": 1185, + "total_ms": 3023 + }, + "slowest_step": "review", + "llm_ms": 2973, + "budget_ms": 3000, + "over_budget": true + } +✔ 접수 현황도 같은 모양 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 795, + "faq_ms": 13, + "ticket_ms": 14, + "answer_ms": 1017, + "review_ms": 1046, + "total_ms": 2904 + }, + "slowest_step": "review", + "llm_ms": 2858, + "budget_ms": 3000, + "over_budget": false + } +Passed 4/4 +report: runs/lab01-challenge2-20261024-174548.json +``` + +**쉽게 말하면** + +- 예산 칸을 만든 뒤 4/4입니다. +- 기본 예산 3초(3000ms)에서 VPN이 3023ms라 `over_budget: true`가 나왔습니다. 23ms 차이로 넘은 것이라 실행마다 true · false가 바뀔 수 있고, 테스트는 그래서 기본값 케이스에서 `budget_ms`만 봅니다. +- `llm_ms`를 `timings` 밖에 두었기 때문에 `slowest_step`이 `llm`으로 바뀌지 않고 그대로 단계 이름입니다. + +- 예산 60000 → false (3357ms, push 직후), 500 → true, 기본 3000 → VPN 3023ms로 **true**, 접수 2904ms로 false. +- `llm_ms`는 `timings` 밖이라 `slowest_step`은 그대로 단계 이름. + +### 10 · 과제 2 뒤 `lab01-challenge` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ FAQ · 접수 시간 따로 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 603, + "faq_ms": 9, + "ticket_ms": 10, + "answer_ms": 969, + "review_ms": 1043, + "total_ms": 2677 + }, + "slowest_step": "review", + "llm_ms": 2615, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 조회 시간 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 614, + "faq_ms": 13, + "ticket_ms": 13, + "answer_ms": 1019, + "review_ms": 876, + "total_ms": 2543 + }, + "slowest_step": "answer", + "llm_ms": 2509, + "budget_ms": 3000, + "over_budget": false + } +✔ 가장 오래 걸린 단계 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 687, + "faq_ms": 17, + "ticket_ms": 10, + "answer_ms": 1146, + "review_ms": 965, + "total_ms": 2838 + }, + "slowest_step": "answer", + "llm_ms": 2798, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 현황도 같은 모양 · 2.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 767, + "faq_ms": 15, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1007, + "total_ms": 2865 + }, + "slowest_step": "answer", + "llm_ms": 2827, + "budget_ms": 3000, + "over_budget": false + } +Passed 4/4 +report: runs/lab01-challenge-20261024-174559.json +``` + +**쉽게 말하면** + +- 과제 2를 해도 과제 1에서 만든 칸이 그대로라 4/4입니다. 고칠 때마다 **앞 테스트도 다시** 돌리는 까닭입니다. + +- 과제 1의 칸이 그대로. + +### 11 · 과제 2 뒤 `lab01` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 3.0s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 785, + "faq_ms": 14, + "ticket_ms": 9, + "answer_ms": 1053, + "review_ms": 1031, + "total_ms": 2902 + }, + "slowest_step": "answer", + "llm_ms": 2869, + "budget_ms": 3000, + "over_budget": false + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 750, + "faq_ms": 15, + "ticket_ms": 14, + "answer_ms": 984, + "review_ms": 895, + "total_ms": 2672 + }, + "slowest_step": "answer", + "llm_ms": 2629, + "budget_ms": 3000, + "over_budget": false + } +✔ 접수 현황 · 3.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "classify_ms": 938, + "faq_ms": 10, + "ticket_ms": 9, + "answer_ms": 981, + "review_ms": 1355, + "total_ms": 3310 + }, + "slowest_step": "review", + "llm_ms": 3274, + "budget_ms": 3000, + "over_budget": true + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174609.json +``` + +**쉽게 말하면** + +- 원래 테스트도 4/4. Lab 01을 마친 워크플로우는 처음 계약을 지키면서 시간 칸 · 예산 칸을 더 가졌습니다. + +- 원래 계약 그대로. + +## Lab 02 · 규칙 먼저, 캐시, LLM 한 번 + +### 12 · 예제 1 · 규칙 먼저 → `lab02` 7/9 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab02-example1-rules.json`): + +```text +my-work/helpdesk.json 의 순서를 바꿔 줘: Has question? → Start clock → Search FAQ → Get ticket → After data → Rule classify(Code) → Known?(If). +Rule classify: 질문에 T-1234 같은 번호(/T-?\d{4}/i)가 있으면 ticket, FAQ 결과가 1개 이상이면 faq, 아니면 unknown. {category, by:'rule'}. +Known? true → Answer, false → Classify → Parse category → After classify → Answer. Answer 는 $json.category 로 분류를 읽는다. +Build result: 분류는 Parse category 가 실행됐으면 그것, 아니면 Rule classify. steps 에 rule 또는 classify. +나머지(답, 검토, timings)는 그대로. 기대 결과 cases/lab02.json 앞 네 케이스. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab02-example1-rules.json +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 41, + "answer_ms": 1766, + "review_ms": 1115, + "total_ms": 2945 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1041, + "review_ms": 1209, + "total_ms": 2295 + } + } +✔ 예제 1 · 없는 접수 번호 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 23, + "answer_ms": 1209, + "review_ms": 797, + "total_ms": 2040 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.4s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 21, + "classify_ms": 582, + "answer_ms": 908, + "review_ms": 877, + "total_ms": 2401 + } + } +✔ 예제 2 · 처음 묻는 VPN · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 1202, + "review_ms": 994, + "total_ms": 2234 + } + } +✘ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 1.9s · took 1.9s (expected at most 1s); cached = false (expected true); llm_calls = 2 (expected 0) + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 문제가 계속되면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 24, + "answer_ms": 967, + "review_ms": 890, + "total_ms": 1898 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 17, + "answer_ms": 1128, + "review_ms": 1048, + "total_ms": 2213 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 829, + "review_ms": 1300, + "total_ms": 2171 + } + } +✘ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.0s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 20, + "answer_ms": 951, + "review_ms": 940, + "total_ms": 1924 + } + } +Passed 7/9 +report: runs/lab02-20261024-174642.json +``` + +**쉽게 말하면** + +- `before`가 먼저 캐시를 비웁니다 (`cleared: 2`는 지운 개수). 그래서 테스트끼리 서로 영향을 주지 않습니다. +- 예제 1(규칙 먼저) 케이스 넷은 모두 ✔입니다. +- ✘ `같은 질문 · took 1.9s (expected at most 1s); cached = false; llm_calls = 2`: 같은 질문이면 캐시에서 1초 안에 와야 하는데 아직 캐시가 없어 LLM을 두 번 불렀다는 뜻입니다. 캐시는 예제 2에서 만드니 **지금은 실패가 맞습니다.** +- ✘ `접수 현황은 캐시 안 함 (2) · answer has none of ['처리 중']`: 02단계와 같은 까닭(검토 LLM이 "처리 중"을 바꿔 씀)입니다. + +- 예제 1 케이스 넷은 모두 통과. 실패 둘: "같은 질문" 캐시 케이스(캐시가 아직 없으니 맞음), 접수 답 문장(검토 LLM). + +### 13 · 예제 1 · 규칙으로 정한 질문 재기 + +실행한 명령: + +```bash +uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5 +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py bench my-helpdesk requests/faq-printer.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.50s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.51s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 2.39s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.12s · route=자동 답변, llm_calls=2, cached=false +p50 2.45s · p95 2.51s · max 2.51s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174654.json +``` + +**쉽게 말하면** + +- 프린터 질문은 FAQ 낱말로 규칙이 분류하니 `llm_calls=2`(답 + 검토)입니다. 세 번이던 것이 두 번이 되어 p50 2.45초입니다. + +- 프린터 p50 2.45초 (LLM 둘). + +### 14 · 예제 2 · 캐시 → `lab02` 9/9 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab02-example2-cache.json`): + +```text +my-work/helpdesk.json 에 캐시를 더해 줘 (AGENTS.md 2절 데이터 API, 3절 캐시 규칙). +Start clock 다음: Cache key(Code: question 을 trim·소문자·연속 공백 하나, use_cache = body.no_cache !== true) +→ Cache lookup(GET http://data-api:8800/cache/, neverError) → Cached?(value 있고 use_cache) + true → Cached result(route·category·answer·sources + llm_calls 0, cached true, steps ['cache'], timings.total_ms) → Send cached + false → 지금 흐름(Search FAQ ...). +Build result 뒤: Cache it?(category faq 이고 use_cache) true → Store in cache(POST /cache/ {value:{route,category,answer,sources}, ttl 600}) → Send result. +접수(ticket)는 저장하지 않는다. 기대 결과 cases/lab02.json 전체, lab01 도 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab02-example2-cache.json +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 3.9s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주십시오.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 76, + "answer_ms": 1837, + "review_ms": 1048, + "total_ms": 2981 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "answer_ms": 911, + "review_ms": 1002, + "total_ms": 1957 + } + } +✔ 예제 1 · 없는 접수 번호 · 2.0s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 현재 확인되지 않습니다. 담당 직원이 확인 후 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 40, + "answer_ms": 1008, + "review_ms": 870, + "total_ms": 1932 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 32, + "classify_ms": 767, + "answer_ms": 810, + "review_ms": 962, + "total_ms": 2586 + } + } +✔ 예제 2 · 처음 묻는 VPN · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 58, + "answer_ms": 1140, + "review_ms": 929, + "total_ms": 2143 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 34 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 2.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1024, + "review_ms": 1125, + "total_ms": 2194 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 35, + "answer_ms": 964, + "review_ms": 1161, + "total_ms": 2173 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "answer_ms": 886, + "review_ms": 1239, + "total_ms": 2168 + } + } +Passed 9/9 +report: runs/lab02-20261024-174725.json +``` + +**쉽게 말하면** + +- 캐시를 넣은 뒤 9/9입니다. +- "같은 질문(띄어쓰기 · 대소문자 다름)"이 0.1초: 질문을 다듬은 글자를 키로 써서 앞 질문의 답을 캐시에서 바로 꺼냈습니다 (`cached: true`, `llm_calls: 0`). +- 접수 현황은 두 번 물어도 캐시하지 않고 매번 새로 조회합니다 (상태가 바뀌기 때문). + +- 같은 질문(띄어쓰기 · 대소문자 다름) 0.1초, `cached: true`, `llm_calls: 0`. 이번에는 접수 답에도 "처리 중"이 남음. + +### 15 · 캐시 뒤 `lab01` → 3/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 2.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 34, + "answer_ms": 1125, + "review_ms": 852, + "total_ms": 2029 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1182, + "review_ms": 890, + "total_ms": 2114 + } + } +✘ 접수 현황 · 2.1s · answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 50, + "answer_ms": 884, + "review_ms": 1046, + "total_ms": 1992 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 3/4 +report: runs/lab01-20261024-174732.json +``` + +**쉽게 말하면** + +- ✘ `접수 현황 · answer has none of ['처리 중']`: 이번에도 **검토 LLM이 "처리 중"을 다른 말로 바꿔 쓴 것**입니다. 캐시와는 상관없습니다 (접수는 캐시하지 않음). +- 나머지 셋은 ✔이라 캐시를 넣어도 원래 계약은 그대로입니다. + +- 접수 답 문장 (검토 LLM). 캐시와는 상관없음. + +### 16 · 캐시 적중 세기 + +실행한 명령: + +```bash +curl -s localhost:8800/cache-stats +``` + +출력 (그대로): + +```text +$ curl -s localhost:8800/cache-stats; echo +{"entries": 2, "hits": 4, "misses": 8} +``` + +**쉽게 말하면** + +- 데이터 API의 캐시 통계입니다. `entries 2`는 저장된 답 2개, `hits 4`는 캐시에서 찾은 횟수, `misses 8`은 찾았는데 없었던 횟수입니다. 적중률은 4 ÷ (4 + 8) ≈ 33%입니다. + +- entries 2 · hits 4 · misses 8. + +### 17 · 예제 3 · 캐시 씀 · 안 씀 + +실행한 명령: + +```bash +curl -s -XPOST localhost:8800/cache-clear +uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 +uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +curl -s localhost:8800/cache-stats +``` + +출력 (그대로): + +```text +$ curl -s -XPOST localhost:8800/cache-clear; echo +{"cleared": 2} + +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.07s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 0.06s · route=자동 답변, llm_calls=0, cached=true +✔ #3 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true +✔ #4 · HTTP 200 · 0.04s · route=자동 답변, llm_calls=0, cached=true +✔ #5 · HTTP 200 · 0.05s · route=자동 답변, llm_calls=0, cached=true +p50 0.05s · p95 2.07s · max 2.07s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174734.json + +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.99s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 1.98s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.21s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 1.97s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.24s · route=자동 답변, llm_calls=2, cached=false +p50 1.99s · p95 2.24s · max 2.24s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174745.json + +$ curl -s localhost:8800/cache-stats; echo +{"entries": 1, "hits": 9, "misses": 1} +``` + +**쉽게 말하면** + +- 캐시를 비우고 같은 VPN 질문 5번: 첫 번만 캐시에 없어 2.07초(`cached=false`), 나머지 넷은 캐시에서 0.04~0.06초(`cached=true`, `llm_calls=0`). +- 그래서 p50은 0.05초인데 p95 · max는 첫 번의 2.07초입니다. 캐시는 "보통"을 크게 줄이지만 "처음 묻는 질문"은 줄이지 못합니다. +- `no_cache`로 다시 재면 매번 LLM이라 p50 1.99초. 마지막 통계 `hits 9 · misses 1`은 `no_cache` 요청도 캐시를 **조회는** 해서 hits로 세기 때문입니다 (쓰지는 않음). + +- 캐시 씀: 첫 번 2.07초, 나머지 0.04~0.06초 (p50 0.05초). 캐시 안 씀: p50 1.99초. +- 그 뒤 hits 9 · misses 1: `no_cache` 요청도 **조회는** 하므로 hits로 셈. + +### 18 · 과제 1 하기 전 → `lab02-challenge` 1/5 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02-challenge.json -v +``` + +출력 (그대로): + +```text +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.0s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 시도해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✘ VPN · LLM 한 번 · 2.5초 안 · 2.2s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 보시기 바랍니다. 추가 도움이 필요하시면 담당자에게 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1054, + "review_ms": 1038, + "total_ms": 2141 + } + } +✘ 비밀번호 · LLM 한 번 · 2.0s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주시기 바랍니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 72, + "answer_ms": 943, + "review_ms": 950, + "total_ms": 1984 + } + } +✘ 접수 현황 · LLM 한 번 · 2.3s · llm_calls = 2 (expected 1) + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 010-2718-2818로 연락해 주시기 바랍니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 26, + "answer_ms": 932, + "review_ms": 1241, + "total_ms": 2213 + } + } +✘ 규칙으로 모르면 분류 + 답 두 번 · 2.8s · llm_calls = 3 (expected 2) + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.", + "sources": [], + "llm_calls": 3, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 27, + "classify_ms": 730, + "answer_ms": 1041, + "review_ms": 926, + "total_ms": 2736 + } + } +✔ 빈 질문은 LLM 없이 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/5 +report: runs/lab02-challenge-20261024-174756.json +``` + +**쉽게 말하면** + +- 과제 1을 하기 **전**이라 1/5가 정상입니다. `before`는 워밍업 호출(첫 호출이 느린 것을 미리 치움)입니다. +- ✘ `llm_calls = 2 (expected 1)`: 이 과제의 목표는 LLM 한 번인데 아직 답 + 검토 두 번을 부른다는 뜻입니다. +- ✘ `llm_calls = 3 (expected 2)`: 규칙으로 모르는 질문은 분류 + 답 + 검토 세 번. 목표는 분류 + 답 두 번입니다. +- 검토 LLM을 지우면 모두 하나씩 줄어듭니다 (20단계). + +- `llm_calls`가 2 · 3 (답 + 검토). 빈 질문만 통과. + +### 19 · 과제 1 하기 전 시간 + +실행한 명령: + +```bash +uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 2.26s · route=자동 답변, llm_calls=2, cached=false +✔ #2 · HTTP 200 · 2.45s · route=자동 답변, llm_calls=2, cached=false +✔ #3 · HTTP 200 · 2.11s · route=자동 답변, llm_calls=2, cached=false +✔ #4 · HTTP 200 · 2.22s · route=자동 답변, llm_calls=2, cached=false +✔ #5 · HTTP 200 · 2.03s · route=자동 답변, llm_calls=2, cached=false +p50 2.22s · p95 2.45s · max 2.45s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174807.json +``` + +**쉽게 말하면** + +- 고치기 전 시간을 재 둡니다 (캐시 없이 p50 2.22초). 고친 뒤(23단계)와 **같은 날, 같은 요청**으로 비교하려는 것입니다. + +- p50 2.22 · p95 2.45초. + +### 20 · 과제 1 · 검토를 검사 Code로 → 15/15 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab02-challenge-fast.json`](72-lab02-challenge-fast.json)): + +```text +my-work/helpdesk.json 에서 Review(검토 LLM)와 Review model 을 지우고, 그 자리에 Review check(Code)를 둬: +Answer 의 output 을 trim, 비었으면 empty, 한글이 없으면 not_korean, 400자 넘으면 자르고 too_long 을 review_problems 에. +Answer 지시문에 "합니다체, 3문장 이내"가 이미 있는지 확인. steps 에는 review 대신 check, llm_calls 는 검토를 세지 않는다. +나머지는 그대로. cases/lab02-challenge.json, lab02.json, lab01.json 모두 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab02-challenge-fast.json +uv run tools/flow.py test cases/lab02-challenge.json -v --repeat 3 +``` + +출력 (그대로): + +```text +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.4s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 3 +✔ VPN · LLM 한 번 · 2.5초 안 #1 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 37, + "answer_ms": 986, + "review_ms": 11, + "total_ms": 1046 + } + } +✔ VPN · LLM 한 번 · 2.5초 안 #2 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1057, + "review_ms": 14, + "total_ms": 1107 + } + } +✔ VPN · LLM 한 번 · 2.5초 안 #3 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 41, + "answer_ms": 952, + "review_ms": 20, + "total_ms": 1023 + } + } +✔ 비밀번호 · LLM 한 번 #1 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 81, + "answer_ms": 868, + "review_ms": 16, + "total_ms": 974 + } + } +✔ 비밀번호 · LLM 한 번 #2 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 824, + "review_ms": 15, + "total_ms": 881 + } + } +✔ 비밀번호 · LLM 한 번 #3 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 48, + "answer_ms": 811, + "review_ms": 12, + "total_ms": 879 + } + } +✔ 접수 현황 · LLM 한 번 #1 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 798, + "review_ms": 15, + "total_ms": 848 + } + } +✔ 접수 현황 · LLM 한 번 #2 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 924, + "review_ms": 12, + "total_ms": 971 + } + } +✔ 접수 현황 · LLM 한 번 #3 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 1001, + "review_ms": 45, + "total_ms": 1096 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #1 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 23, + "classify_ms": 723, + "answer_ms": 838, + "review_ms": 15, + "total_ms": 1606 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #2 · 1.4s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 594, + "answer_ms": 698, + "review_ms": 13, + "total_ms": 1339 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 #3 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 746, + "answer_ms": 801, + "review_ms": 15, + "total_ms": 1599 + } + } +✔ 빈 질문은 LLM 없이 #1 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +✔ 빈 질문은 LLM 없이 #2 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +✔ 빈 질문은 LLM 없이 #3 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 15/15 +report: runs/lab02-challenge-20261024-174837.json +``` + +**쉽게 말하면** + +- 검토 LLM을 검사 Code로 바꾼 뒤 15/15 (`--repeat 3`이라 5케이스 × 3번). +- VPN · 비밀번호 · 접수가 0.9~1.2초로 2.5초 기대 안에 넉넉히 들어옵니다. LLM이 한 번(`llm_calls: 1`)이 되었기 때문입니다. +- 세 번 반복해도 모두 통과하니 "운 좋게 한 번 통과"가 아닙니다. + +- VPN 1.1~1.2초 · 비밀번호 0.9~1.0초 · 접수 0.9~1.1초 (`llm_calls: 1`), 규칙으로 모르는 질문 1.4~1.6초 (2). + +### 21 · 과제 1 뒤 `lab02` → 9/9 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 999, + "review_ms": 15, + "total_ms": 1047 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 29, + "answer_ms": 802, + "review_ms": 50, + "total_ms": 899 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 901, + "review_ms": 15, + "total_ms": 951 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "classify_ms": 572, + "answer_ms": 898, + "review_ms": 14, + "total_ms": 1522 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1106, + "review_ms": 11, + "total_ms": 1149 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 22, + "answer_ms": 1013, + "review_ms": 21, + "total_ms": 1063 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 989, + "review_ms": 33, + "total_ms": 1069 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 24, + "answer_ms": 786, + "review_ms": 17, + "total_ms": 838 + } + } +Passed 9/9 +report: runs/lab02-20261024-174846.json +``` + +**쉽게 말하면** + +- 검토 LLM이 없어져 접수 답의 "처리 중"이 바뀌지 않고 그대로 나옵니다. 그래서 그동안 가끔 실패하던 접수 케이스도 이제 통과합니다. + +- 검토 LLM이 없으니 접수 답의 "처리 중"이 그대로. + +### 22 · 과제 1 뒤 `lab01` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab01.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN FAQ · 단계별 시간 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 32, + "answer_ms": 1122, + "review_ms": 11, + "total_ms": 1173 + } + } +✔ VPN FAQ · 전체 시간 · LLM 호출 수 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 1219, + "review_ms": 14, + "total_ms": 1268 + } + } +✔ 접수 현황 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 907, + "review_ms": 15, + "total_ms": 956 + } + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab01-20261024-174849.json +``` + +**쉽게 말하면** + +- 원래 테스트도 4/4. `lab01`은 `steps`에 `answer`가 있는지만 보므로, `review`가 `check`로 바뀌어도 깨지지 않습니다. + +- `lab01`은 `steps`에 `answer`만 봄. + +### 23 · 과제 1 뒤 시간 + +실행한 명령: + +```bash +uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py bench my-helpdesk requests/faq-vpn-nocache.json -n 5 +POST http://localhost:5678/webhook/my-helpdesk × 5 +✔ #1 · HTTP 200 · 1.32s · route=자동 답변, llm_calls=1, cached=false +✔ #2 · HTTP 200 · 1.14s · route=자동 답변, llm_calls=1, cached=false +✔ #3 · HTTP 200 · 1.11s · route=자동 답변, llm_calls=1, cached=false +✔ #4 · HTTP 200 · 1.12s · route=자동 답변, llm_calls=1, cached=false +✔ #5 · HTTP 200 · 1.05s · route=자동 답변, llm_calls=1, cached=false +p50 1.12s · p95 1.32s · max 1.32s · failures 0/5 +report: runs/bench-my-helpdesk-20261024-174855.json +``` + +**쉽게 말하면** + +- 같은 요청으로 다시 재면 p50 2.22 → **1.12초**. LLM 한 번을 빼서 약 1초 줄었습니다 (교안 1단계의 어림과 맞음). + +- p50 2.22 → **1.12초**, p95 2.45 → 1.32초. + +### 24 · 과제 2 하기 전 → `lab02-challenge2` 4/6 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02-challenge2.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 처음 묻는 VPN (캐시 없음) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 39, + "answer_ms": 947, + "review_ms": 16, + "total_ms": 1009 + } + } +✘ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 1.2s · took 1.2s (expected at most 1s); cached = false (expected true); cache_by = null (expected faq); llm_calls = 1 (expected 0) + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1067, + "review_ms": 14, + "total_ms": 1117 + } + } +✘ 같은 말은 질문 캐시 · 0.1s · cache_by = null (expected question) + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 14 + } + } +✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1014, + "review_ms": 9, + "total_ms": 1051 + } + } +✔ 다른 FAQ는 따로 (프린터 처음) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 26, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1091 + } + } +✔ 접수 현황은 캐시 안 함 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 869, + "review_ms": 13, + "total_ms": 913 + } + } +Passed 4/6 +report: runs/lab02-challenge2-20261024-174901.json +``` + +**쉽게 말하면** + +- 과제 2를 하기 **전**이라 4/6이 정상입니다. +- ✘ `다른 말로 묻는 VPN · cached = false; cache_by = null; llm_calls = 1`: 글자가 다른 질문이라 질문 캐시에 맞지 않아 LLM을 불렀다는 뜻입니다. 같은 FAQ(FAQ-003)로 답하는 질문이니 FAQ 번호 캐시가 있으면 맞았을 것입니다. +- ✘ `같은 말은 질문 캐시 · cache_by = null (expected question)`: 캐시에서는 왔지만(0.1초) **어느 캐시에서 왔는지** 알려 주는 `cache_by` 칸이 아직 없습니다. + +- "다른 말 VPN"은 `cached: false`, LLM 1번, 1.2초. "같은 말"은 캐시는 맞지만 `cache_by` 칸이 없음. + +### 25 · 과제 2 · FAQ 번호 캐시 → 6/6 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab02-challenge2-faqcache.json`](72-lab02-challenge2-faqcache.json)): + +```text +my-work/helpdesk.json 에 FAQ 번호 캐시를 더해 줘 (AGENTS.md 3절 캐시 키 표). +Rule classify 다음: FAQ cache key(Code: category 가 faq 이고 use_cache 이고 FAQ 결과가 있으면 faq_key = 'faq-' + 첫 FAQ id, 아니면 'none') +→ FAQ cache lookup(GET /cache/, neverError) → FAQ cached?(value 있고 faq_key 가 'none' 아님) + true → FAQ cached result(llm_calls 0, cached true, cache_by 'faq', steps ['faq', 'rule', 'cache']) → Send + false → Back to rule(Code: Rule classify 의 결과를 그대로) → Known? +Store in cache 뒤에 Store by FAQ(POST /cache/, 같은 value, ttl 600) → Result again. +질문 캐시의 Cached result 에는 cache_by 'question', Build result 에는 cache_by null. +기대 결과 cases/lab02-challenge2.json, lab02-challenge.json, lab02.json 도 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab02-challenge2-faqcache.json +uv run tools/flow.py test cases/lab02-challenge2.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 3} +POST http://localhost:5678/webhook/my-helpdesk · 6 case(s) × 1 +✔ 처음 묻는 VPN (캐시 없음) · 2.5s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1556, + "review_ms": 23, + "total_ms": 1665 + } + } +✔ 다른 말로 묻는 VPN → FAQ 번호 캐시 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "faq", + "steps": [ + "faq", + "rule", + "cache" + ], + "timings": { + "total_ms": 44 + } + } +✔ 같은 말은 질문 캐시 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } +✔ no_cache면 FAQ 번호 캐시도 안 씀 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 연결이 계속 끊어지는데요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 연결이 계속 끊어질 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1052, + "review_ms": 20, + "total_ms": 1110 + } + } +✔ 다른 FAQ는 따로 (프린터 처음) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 42, + "answer_ms": 1035, + "review_ms": 14, + "total_ms": 1102 + } + } +✔ 접수 현황은 캐시 안 함 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 34, + "answer_ms": 796, + "review_ms": 14, + "total_ms": 852 + } + } +Passed 6/6 +report: runs/lab02-challenge2-20261024-174920.json +``` + +**쉽게 말하면** + +- FAQ 번호 캐시를 넣은 뒤 6/6. +- "VPN 연결이 계속 끊어지는데요"처럼 다른 말로 물어도 같은 FAQ라 0.1초, `cache_by: faq`, `llm_calls: 0`입니다. +- `no_cache`를 보내면 두 캐시 모두 쓰지 않고(1.2초), 프린터처럼 다른 FAQ는 따로 계산합니다. + +- "VPN 연결이 계속 끊어지는데요" → `cache_by: faq`, `llm_calls: 0`, 0.1초. 같은 말 → `cache_by: question`, 0.0초. + +### 26 · 과제 2 뒤 `lab02-challenge` → 5/5 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02-challenge.json -v +``` + +출력 (그대로): + +```text +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +POST http://localhost:5678/webhook/my-helpdesk · 5 case(s) × 1 +✔ VPN · LLM 한 번 · 2.5초 안 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1017, + "review_ms": 16, + "total_ms": 1067 + } + } +✔ 비밀번호 · LLM 한 번 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 840, + "review_ms": 40, + "total_ms": 928 + } + } +✔ 접수 현황 · LLM 한 번 · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 28, + "answer_ms": 835, + "review_ms": 19, + "total_ms": 891 + } + } +✔ 규칙으로 모르면 분류 + 답 두 번 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "classify_ms": 842, + "answer_ms": 771, + "review_ms": 11, + "total_ms": 1659 + } + } +✔ 빈 질문은 LLM 없이 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 5/5 +report: runs/lab02-challenge-20261024-174926.json +``` + +**쉽게 말하면** + +- 과제 1 테스트도 5/5. 이 테스트의 요청은 모두 `no_cache`라 새 캐시가 끼어들지 않습니다. + +- 모두 `no_cache`라 두 번째 캐시와 상관없음. + +### 27 · 과제 2 뒤 `lab02` → 9/9 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 1111, + "review_ms": 17, + "total_ms": 1168 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 895, + "review_ms": 16, + "total_ms": 945 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "answer_ms": 946, + "review_ms": 17, + "total_ms": 998 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 25, + "classify_ms": 623, + "answer_ms": 1026, + "review_ms": 14, + "total_ms": 1699 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 1112, + "review_ms": 23, + "total_ms": 1168 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.0s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "cache_by": "question", + "steps": [ + "cache" + ], + "timings": { + "total_ms": 15 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 21, + "answer_ms": 1065, + "review_ms": 32, + "total_ms": 1137 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 30, + "answer_ms": 936, + "review_ms": 15, + "total_ms": 988 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 0.9s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "cache_by": null, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 822, + "review_ms": 14, + "total_ms": 873 + } + } +Passed 9/9 +report: runs/lab02-20261024-174935.json +``` + +**쉽게 말하면** + +- Lab 02 처음 테스트도 9/9. 두 번째 캐시를 더해도 질문 캐시 케이스가 그대로입니다. + +- 원래 캐시 케이스 그대로. + +## Lab 03 · 멈춰도 답하게 + +### 28 · 예제 1 · 고치기 전에 고장 겪기 + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab02-example2-cache.json +uv run tools/flow.py fault faq slow +uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +uv run tools/flow.py fault faq error +uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +uv run tools/flow.py fault all ok +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py fault faq slow +✔ fault switch: {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} + +$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +HTTP 200 · 11.5s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 언제든 문의해 주세요.", + "sources": [ + "FAQ-001" + ], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "review" + ], + "timings": { + "data_ms": 8144, + "answer_ms": 1518, + "review_ms": 1013, + "total_ms": 10701 + } + } + +$ uv run tools/flow.py fault faq error +✔ fault switch: {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} + +$ uv run tools/flow.py call my-helpdesk '{"question": "비밀번호를 잊어버렸어요", "no_cache": true}' -v +HTTP 500 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } + +$ uv run tools/flow.py fault all ok +✔ fault switch: {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- **고치기 전에 고장을 먼저 겪어 보는** 단계라 결과가 나쁜 것이 정상입니다. +- FAQ를 8초 느리게 하면 11.5초를 기다린 뒤에야 평소 답이 옵니다. 기다리는 한도(timeout)가 없어 끝까지 기다렸기 때문입니다. +- FAQ가 오류(500)를 내면 0.3초 만에 **HTTP 500**: 워크플로우가 오류에서 멈춰 사용자는 아무 안내도 받지 못합니다. +- 마지막 `fault all ok`로 고장을 꼭 되돌립니다. + +- FAQ 8초 늦음 → **11.5초** 뒤 평소 답 (10/23 13.2초). FAQ 500 → **HTTP 500** 0.3초. + +### 29 · 예제 1 · timeout · 재시도 · 담당자 → `lab03` 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example1-resilient.json`): + +```text +my-work/helpdesk.json 의 Search FAQ 와 Get ticket 에 options.timeout 2000, 노드 설정 retryOnFail true, maxTries 2, waitBetweenTries 500, +onError continueErrorOutput 을 넣어 줘. 두 노드의 오류 출력(main 1번)은 Data down(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason data_api_down) +→ Send data down(Respond). Get ticket 은 404 를 받아야 하니 neverError 는 그대로 둔다. 기대 결과 cases/lab03.json, lab02 도 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab03-example1-resilient.json +uv run tools/flow.py test cases/lab03.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 3.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 605, + "answer_ms": 1634, + "review_ms": 13, + "total_ms": 2270 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 4} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 2.1s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175023.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- timeout · 재시도 · 오류 출력을 넣은 뒤 4/4. +- `before` · `after` 줄은 테스트가 케이스 앞에서 고장을 켜고 뒤에서 끄는 것입니다. +- 느림은 4.6초(2초 기다림 + 0.5초 쉼 + 2초 다시) 뒤, 오류는 0.6초 뒤 `담당자 연결`로 정해진 답을 합니다. 캐시에 있는 질문은 FAQ가 멈춰도 0.1초에 답합니다. + +- 느림 4.6초 · 오류 0.6초 → `담당자 연결`, 캐시에 있으면 0.1초. +- 평소 VPN의 `data_ms`가 **605ms** (예제 2 단계 32~76ms): `Get ticket`의 재시도가 `/tickets/NONE` 404에도 걸림 (예제 3에서 고침). + +### 30 · 평소 그대로 → `lab02` 9/9 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 585, + "answer_ms": 1071, + "review_ms": 20, + "total_ms": 1687 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 71, + "answer_ms": 1030, + "review_ms": 19, + "total_ms": 1128 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 555, + "answer_ms": 1179, + "review_ms": 24, + "total_ms": 1774 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 554, + "classify_ms": 650, + "answer_ms": 927, + "review_ms": 14, + "total_ms": 2152 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 553, + "answer_ms": 1015, + "review_ms": 28, + "total_ms": 1605 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 19 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 551, + "answer_ms": 1081, + "review_ms": 34, + "total_ms": 1685 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1062, + "review_ms": 14, + "total_ms": 1114 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 47, + "answer_ms": 977, + "review_ms": 15, + "total_ms": 1049 + } + } +Passed 9/9 +report: runs/lab02-20261024-175036.json +``` + +**쉽게 말하면** + +- 고장 대비를 넣어도 평소 테스트(`lab02`)는 9/9. timeout 2초는 평소(수십 ms)에는 걸리지 않습니다. + +- timeout 2초는 평소에는 영향 없음. + +### 31 · 예제 2 · 고치기 전에 LLM 멈춤 겪기 + +실행한 명령: + +```bash +uv run tools/flow.py llm down +uv run tools/flow.py call my-helpdesk requests/faq-vpn-nocache.json -v +uv run tools/flow.py llm up +``` + +출력 (그대로): + +```text +$ uv run tools/flow.py llm down +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up + +$ uv run tools/flow.py call my-helpdesk requests/faq-vpn-nocache.json -v +HTTP 500 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } + +$ uv run tools/flow.py llm up +✔ OpenAI credential: real key from .env restored +``` + +**쉽게 말하면** + +- 고치기 전에 LLM 멈춤을 겪어 봅니다. `llm down`은 OpenAI 키를 일부러 틀리게 바꿉니다. +- 1.3초 만에 **HTTP 500**: LLM 노드가 실패하자 워크플로우가 멈췄습니다. 마지막 `llm up`으로 진짜 키를 되돌립니다. + +- LLM 401 → HTTP 500 1.3초. + +### 32 · 예제 2 · 고치기 전 → `lab03-llm-down` 1/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-llm-down.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✘ VPN · FAQ 원문으로 · 0.9s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✘ 접수 현황 · 상태 그대로 · 0.4s · status 500 (expected 200); route = null (expected 간단 답변); answer has none of ['처리 중'] + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✘ 모르는 질문 · 담당자 · 0.9s · status 500 (expected 200); route = null (expected 담당자 연결) + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 500 + { + "message": "Error in workflow" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 1/4 +report: runs/lab03-llm-down-20261024-175047.json +✔ OpenAI credential: real key from .env restored +``` + +**쉽게 말하면** + +- 고치기 **전**이라 1/4가 정상입니다. +- ✘ `status 500 (expected 200); route = null`: 응답이 HTTP 500이라 `route` 같은 칸이 하나도 없다는 뜻입니다. 기대는 LLM 없이도 200으로 `간단 답변`을 하는 것입니다. +- ✘ `answer has none of ['인증서']` · `['처리 중']`: 답 자체가 없으니 FAQ 원문 · 접수 상태 글자도 없습니다. +- ✔ 빈 질문은 LLM 전에 거절하니 고장과 상관없이 통과합니다. + +- 빈 질문만 통과. + +### 33 · 예제 2 · LLM 없이 간단 답변 → 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example2-llm-fallback.json`): + +```text +my-work/helpdesk.json 의 Classify 와 Answer Agent 에 onError continueErrorOutput 을 넣고, 두 노드의 오류 출력을 Simple answer(Code) → Send simple answer 로 이어 줘. +Simple answer: 질문에 접수 번호가 있고 Get ticket 결과에 status 가 있으면 route 간단 답변, "<번호> 접수 건은 현재 '' 상태입니다. ". +FAQ 결과가 있으면 route 간단 답변, 첫 FAQ answer + " (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", sources [그 id]. +아니면 route 담당자 연결. 모두 llm_calls 0, reason llm_down. 기대 결과 cases/lab03-llm-down.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab03-example2-llm-fallback.json +uv run tools/flow.py test cases/lab03-llm-down.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN · FAQ 원문으로 · 2.6s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 접수 현황 · 상태 그대로 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 모르는 질문 · 담당자 · 0.8s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab03-llm-down-20261024-175108.json +✔ OpenAI credential: real key from .env restored +``` + +**쉽게 말하면** + +- LLM 노드에 오류 출력을 달고 간단 답변을 만든 뒤 4/4. +- VPN은 FAQ 원문을 그대로, 접수는 상태를 그대로(0.3초), 모르는 질문은 담당자로 보냅니다. LLM 없이 **데이터로 답할 수 있는 것은 데이터로** 답한 것입니다. + +- VPN 2.6초 FAQ 원문 · 접수 0.3초 상태 그대로 · 점심 0.8초 담당자. + +### 34 · `llm up` 뒤 `lab02` → 9/9 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab02.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 9 case(s) × 1 +✔ 예제 1 · 프린터 FAQ는 규칙으로 · 1.8s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 568, + "answer_ms": 1078, + "review_ms": 12, + "total_ms": 1679 + } + } +✔ 예제 1 · 접수 번호는 규칙으로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 969, + "review_ms": 15, + "total_ms": 1032 + } + } +✔ 예제 1 · 없는 접수 번호 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드리겠습니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 546, + "answer_ms": 1011, + "review_ms": 46, + "total_ms": 1632 + } + } +✔ 예제 1 · 규칙으로 모르면 LLM이 분류 · 2.2s + → request POST /webhook/my-helpdesk + { + "question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.", + "sources": [], + "llm_calls": 2, + "cached": false, + "steps": [ + "rule", + "classify", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "classify_ms": 881, + "answer_ms": 734, + "review_ms": 23, + "total_ms": 2206 + } + } +✔ 예제 2 · 처음 묻는 VPN · 1.9s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1222, + "review_ms": 12, + "total_ms": 1803 + } + } +✔ 예제 2 · 같은 질문 (띄어쓰기·대소문자 다름) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": " vpn 접속이 자꾸 끊겨요. 어떻게 하나요? ", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 20 + } + } +✔ 예제 2 · no_cache면 캐시 안 씀 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 560, + "answer_ms": 1041, + "review_ms": 17, + "total_ms": 1625 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (1) · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락 바랍니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 27, + "answer_ms": 934, + "review_ms": 19, + "total_ms": 986 + } + } +✔ 예제 2 · 접수 현황은 캐시 안 함 (2) · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 31, + "answer_ms": 1016, + "review_ms": 43, + "total_ms": 1110 + } + } +Passed 9/9 +report: runs/lab02-20261024-175123.json +``` + +**쉽게 말하면** + +- `llm up` 뒤 평소 테스트 9/9. 대안 길을 넣어도 평소에는 원래 길로 갑니다. + +- 평소대로. + +### 35 · 데이터 고장도 그대로 → `lab03` 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 571, + "answer_ms": 1006, + "review_ms": 16, + "total_ms": 1600 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 2} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.9s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175132.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 데이터 고장 테스트(`lab03`)도 그대로 4/4. 예제 2를 하면서 예제 1의 설정을 망가뜨리지 않았다는 확인입니다. + +- 예제 1의 설정이 남아 있음. + +### 36 · 예제 3 · 고치기 전 → `lab03-ticket` 2/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-ticket.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 접수 현황 · 1.0s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 33, + "answer_ms": 915, + "review_ms": 12, + "total_ms": 970 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✘ 접수 API 500 · 접수 질문은 담당자 · 0.6s · reason = data_api_down (expected tickets_down) + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✘ 접수 API 500 · FAQ 질문은 그대로 답 · 0.6s · route = 담당자 연결 (expected 자동 답변); category = null (expected faq); answer has none of ['인증서'] + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.6s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 564, + "answer_ms": 978, + "review_ms": 10, + "total_ms": 1558 + } + } +Passed 2/4 +report: runs/lab03-ticket-20261024-175136.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 고치기 **전**이라 2/4가 정상입니다. 이번 고장은 접수 API만 500입니다. +- ✘ `reason = data_api_down (expected tickets_down)`: 담당자 연결은 했지만 까닭을 "데이터 전체 고장"으로 적었습니다. 접수 API만 멈춘 것을 구분하지 못한 것입니다. +- ✘ `FAQ 질문은 그대로 답 · route = 담당자 연결 (expected 자동 답변)`: FAQ는 멀쩡한데 VPN 질문까지 담당자로 갔습니다. 모든 질문이 접수 조회를 지나가고, 오류 출력은 어떤 질문인지 가리지 않고 같은 길로 보내기 때문입니다. +- ✔ 없는 접수 번호(404)는 고장이 아니라 평소 답이 맞습니다. + +- 접수 API 500에서 접수 질문은 `reason: data_api_down`, **VPN 질문도 `담당자 연결`** (0.6초). 오류 출력은 질문을 가리지 않음. + +### 37 · 예제 3 · 접수 API만 고장 → 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 `reference/lab03-example3-ticket-down.json`): + +```text +my-work/helpdesk.json 의 Get ticket 응답 설정에 fullResponse 와 neverError 를 켜 줘 (결과가 {statusCode, body}). +Get ticket 보통 출력 다음에 Ticket check(Code): ticket = statusCode 500 이상이면 {} 아니면 body, ticket_status = statusCode, +tickets_down = statusCode 500 이상이고 질문에 접수 번호(/T-?\d{4}/i)가 있을 때만 true. +→ Tickets down?(If) true → Tickets down(Set: route 담당자 연결, category ticket, answer "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", reason tickets_down, llm_calls 0) → Send tickets down + false → 지금 흐름(After data …). +$('Get ticket').first().json 을 읽던 곳은 모두 $('Ticket check').first().json.ticket 으로 바꾼다. 기대 결과 cases/lab03-ticket.json, lab03.json, lab03-llm-down.json 도 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push reference/lab03-example3-ticket-down.json +uv run tools/flow.py test cases/lab03-ticket.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 접수 현황 · 2.4s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-1002 티켓은 현재 \"처리 중\" 상태이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의가 필요하면 연락처 010-2718-2818로 연락해 주세요.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 102, + "answer_ms": 1458, + "review_ms": 13, + "total_ms": 1594 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✔ 접수 API 500 · 접수 질문은 담당자 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "ticket", + "answer": "접수 현황 시스템이 응답하지 않아 담당자에게 연결합니다. 잠시 뒤 다시 물어봐 주세요.", + "reason": "tickets_down", + "llm_calls": 0 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "slow_seconds": 8.0} +✔ 접수 API 500 · FAQ 질문은 그대로 답 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 38, + "answer_ms": 1023, + "review_ms": 12, + "total_ms": 1083 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ 없는 접수 번호는 그대로 (404는 고장이 아님) · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "T-9999 접수 건은 어떻게 됐나요?", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "ticket", + "answer": "T-9999 접수 건은 찾지 못했습니다. 담당 직원이 도와드릴 예정입니다.", + "sources": [], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 44, + "answer_ms": 1192, + "review_ms": 22, + "total_ms": 1270 + } + } +Passed 4/4 +report: runs/lab03-ticket-20261024-175154.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- `fullResponse`로 상태 코드를 보고 나눈 뒤 4/4. +- 접수 질문만 `tickets_down`으로 0.1초에 담당자, VPN 질문은 평소처럼 `자동 답변`. **멈춘 부분만 넘기고 나머지는 평소대로** 한 것입니다. + +- 접수 질문 `tickets_down` 0.1초, VPN 질문 `자동 답변` 1.1초 (`data_ms` 38ms), 없는 번호(404)는 평소 답. + +### 38 · 예제 3 뒤 `lab03` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 61, + "answer_ms": 1037, + "review_ms": 14, + "total_ms": 1120 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.8s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 17 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175202.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 예제 3을 한 뒤에도 FAQ 고장 처리(`lab03`)가 그대로 4/4입니다. + +- FAQ 고장 처리 그대로. + +### 39 · 예제 3 뒤 `lab03-llm-down` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-llm-down.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ VPN · FAQ 원문으로 · 0.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "faq", + "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다.)", + "sources": [ + "FAQ-003" + ], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 접수 현황 · 상태 그대로 · 0.5s + → request POST /webhook/my-helpdesk + { + "question": "T-1002 처리 상태 알려 주세요", + "user": "박서연" + } + ← response HTTP 200 + { + "route": "간단 답변", + "category": "ticket", + "answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 모르는 질문 · 담당자 · 0.3s + → request POST /webhook/my-helpdesk + { + "question": "오늘 점심 메뉴 추천해 주세요", + "user": "이준호" + } + ← response HTTP 200 + { + "route": "담당자 연결", + "category": "other", + "answer": "지금은 자동 답변을 할 수 없어 담당자에게 연결합니다.", + "sources": [], + "llm_calls": 0, + "reason": "llm_down" + } +✔ 빈 질문 · 0.0s + → request POST /webhook/my-helpdesk + { + "question": "", + "user": "이준호" + } + ← response HTTP 400 + { + "error": "question is required" + } +Passed 4/4 +report: runs/lab03-llm-down-20261024-175206.json +✔ OpenAI credential: real key from .env restored +``` + +**쉽게 말하면** + +- LLM 멈춤 테스트도 4/4. 간단 답변이 바뀐 자리(`Ticket check`)에서 접수 건을 읽도록 함께 고쳤기 때문입니다. + +- 간단 답변이 `Ticket check`의 접수 건을 읽음. + +### 40 · 과제 1 · 상태 점검 → `lab03-challenge` 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/health.json` · 이 기록에서 push한 같은 역할의 정답 [`72-lab03-challenge-health.json`](72-lab03-challenge-health.json)): + +```text +my-work/health.json 을 새로 만들어 줘 (웹훅 my-health, AGENTS.md 3절). +Start(Code t0) → Check data API(GET http://data-api:8800/health, timeout 2000, onError continueRegularOutput, alwaysOutputData) +→ Check FAQ(GET /faq?q=vpn, 같은 설정) → Check LLM(Agent "Reply with the single word OK.", onError continueRegularOutput, alwaysOutputData; + 모델 options timeout 8000, maxRetries 0) → Summarize(Code): data_api = status ok, faq = results 가 배열, llm = output 에 ok. + failed = 거짓인 것 이름. status: 모두 참 ok, data_api 나 llm 중 하나라도 참이면 degraded, 아니면 down. seconds. +기대 결과 cases/lab03-challenge.json. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab03-challenge-health.json +uv run tools/flow.py test cases/lab03-challenge.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-health · 4 case(s) × 1 +✔ 평소 · 2.2s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "ok", + "checks": { + "data_api": true, + "faq": true, + "llm": true + }, + "failed": [], + "seconds": 1.4 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 오류 · 1.0s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 1 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 (8초) · 점검은 빨리 끝남 · 3.0s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": false, + "llm": true + }, + "failed": [ + "faq" + ], + "seconds": 2.9 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up +✔ LLM 멈춤 · 0.4s + → request POST /webhook/my-health + {} + ← response HTTP 200 + { + "status": "degraded", + "checks": { + "data_api": true, + "faq": true, + "llm": false + }, + "failed": [ + "llm" + ], + "seconds": 0.3 + } +✔ OpenAI credential: real key from .env restored +Passed 4/4 +report: runs/lab03-challenge-20261024-175230.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 상태 점검 `my-health` 4/4. +- 평소에는 `ok`, FAQ 오류 · LLM 멈춤에서는 `degraded`(일부만 됨)를 1초 안팎에 알려 줍니다. FAQ가 8초 느려도 점검은 3.0초에 끝납니다 (점검마다 짧은 timeout). + +- 평소 `ok` 2.2초 · FAQ 오류 `degraded` 1.0초 · FAQ 느림 3.0초 · LLM 멈춤 0.4초. + +### 41 · 과제 2 하기 전 → `lab03-challenge2` 3/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03-challenge2.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.3s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 72, + "answer_ms": 1098, + "review_ms": 18, + "total_ms": 1212 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✘ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 4.6s · took 4.6s (expected at most 1s); reason = data_api_down (expected breaker_open) + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 43, + "answer_ms": 1092, + "review_ms": 15, + "total_ms": 1161 + } + } +Passed 3/4 +report: runs/lab03-challenge2-20261024-175242.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +``` + +**쉽게 말하면** + +- 과제 2를 하기 **전**이라 3/4가 정상입니다. +- ✘ `다음 질문은 기다리지 않음 · took 4.6s (expected at most 1s); reason = data_api_down (expected breaker_open)`: FAQ가 느리다는 것을 앞 질문에서 이미 알았는데도 다음 사람도 4.6초를 똑같이 기다렸다는 뜻입니다. 차단기가 없어서 매번 FAQ를 다시 부르기 때문입니다. + +- FAQ 느림에서 다음 질문도 4.6초 기다린 뒤 `data_api_down`. + +### 42 · 과제 2 · 차단기 → 4/4 + +codex에 입력하는 프롬프트 (터미널 ①, 학생이 만들 파일 `my-work/helpdesk.json (고침)` · 이 기록에서 push한 같은 역할의 정답 [`72-lab03-challenge2-breaker.json`](72-lab03-challenge2-breaker.json)): + +```text +my-work/helpdesk.json 에 FAQ 차단기를 더해 줘 (AGENTS.md 3절 캐시 키 표). +Cached? false → Breaker check(GET http://data-api:8800/cache/breaker-faq, neverError) → Breaker open?(value 있음) + true → Breaker open(Set: route 담당자 연결, answer "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", reason breaker_open, llm_calls 0) → Send breaker open + false → Search FAQ. +Search FAQ 의 오류 출력 → Open breaker(POST /cache/breaker-faq {value:{open:true, since: Date.now()}, ttl 30}, neverError) → Data down. +닫는 노드는 만들지 않는다 (ttl 30초가 지나면 데이터 API가 지움). 기대 결과 cases/lab03-challenge2.json, lab03.json 도 통과. +``` + +실행한 명령: + +```bash +uv run tools/flow.py push answers/72-lab03-challenge2-breaker.json +uv run tools/flow.py test cases/lab03-challenge2.json -v +``` + +출력 (그대로): + +```text +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 2.7s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 114, + "answer_ms": 1678, + "review_ms": 14, + "total_ms": 1827 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ 느림 · 처음 한 번은 기다린 뒤 담당자 · 4.8s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ FAQ 느림 · 다음 질문은 기다리지 않음 (차단기 열림) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ FAQ가 돌아오고 차단기를 비우면 평소대로 · 1.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 52, + "answer_ms": 1005, + "review_ms": 18, + "total_ms": 1084 + } + } +Passed 4/4 +report: runs/lab03-challenge2-20261024-175304.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ after: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 0} +``` + +**쉽게 말하면** + +- 차단기를 넣은 뒤 4/4. +- 처음 한 번은 4.8초 기다린 뒤 담당자(`data_api_down`), 그때 차단기를 엽니다. 다음 질문은 FAQ를 부르지 않고 **0.1초** 만에 담당자(`breaker_open`). +- FAQ가 돌아오고 캐시(차단기 포함)를 비우면 다시 평소 답(1.1초). + +- 처음 4.8초 `data_api_down` → 다음 질문 **0.1초** `breaker_open` → FAQ가 돌아오고 캐시를 비우면 1.1초 평소 답. + +### 43 · 과제 2 뒤 `lab03` → 4/4 + +실행한 명령: + +```bash +uv run tools/flow.py test cases/lab03.json -v +``` + +출력 (그대로): + +```text +POST http://localhost:5678/webhook/my-helpdesk · 4 case(s) × 1 +✔ 평소 · 1.2s + → request POST /webhook/my-helpdesk + { + "question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?", + "user": "이준호", + "no_cache": true + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 추가 도움이 필요하면 담당자가 도와드리겠습니다.", + "sources": [ + "FAQ-003" + ], + "llm_calls": 1, + "cached": false, + "steps": [ + "rule", + "faq", + "ticket", + "answer", + "check" + ], + "timings": { + "data_ms": 77, + "answer_ms": 1000, + "review_ms": 16, + "total_ms": 1104 + } + } +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 느릴 때 (8초) · 4.6s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "data_api_down" + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ FAQ가 오류일 때 (500) · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "비밀번호를 잊어버렸어요", + "user": "한지훈", + "no_cache": true + } + ← response HTTP 200 + { + "route": "담당자 연결", + "answer": "사내 데이터 시스템이 응답하지 않아 담당자에게 연결합니다.", + "reason": "breaker_open", + "llm_calls": 0 + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +✔ before: POST http://localhost:8800/cache-clear · HTTP 200 · 0.0s · {"cleared": 1} +✔ before: POST /webhook/my-helpdesk · HTTP 200 · 1.3s · {"route": "자동 답변", "category": "faq", "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리 +✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "error", "tickets": "ok", "slow_seconds": 8.0} +✔ 캐시에 있으면 FAQ가 멈춰도 답 · 0.1s + → request POST /webhook/my-helpdesk + { + "question": "프린터 출력이 안 돼요", + "user": "최유진" + } + ← response HTTP 200 + { + "route": "자동 답변", + "category": "faq", + "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보세요. 추가 도움이 필요하면 담당 직원이 도와드리겠습니다.", + "sources": [ + "FAQ-004" + ], + "llm_calls": 0, + "cached": true, + "steps": [ + "cache" + ], + "timings": { + "total_ms": 21 + } + } +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +Passed 4/4 +report: runs/lab03-20261024-175311.json +✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "slow_seconds": 8.0} +``` + +**쉽게 말하면** + +- 데이터 고장 테스트도 4/4. +- FAQ 오류 케이스가 이번에는 0.1초입니다. 바로 앞 케이스(FAQ 느림)가 연 차단기가 아직 30초 안이라 `breaker_open`으로 바로 넘어갔기 때문입니다. 테스트는 `route`(담당자 연결)만 보므로 통과입니다. + +- FAQ 오류 케이스는 앞 케이스(FAQ 느림)가 연 차단기 때문에 **0.1초** `breaker_open` (29단계 예제 1 정답은 0.6초 `data_api_down`). 테스트는 `route`만 봐서 통과. + +### 44 · 끝나면 doctor → ready + +실행한 명령: + +```bash +uv run tools/flow.py doctor +``` + +출력 (그대로): + +```text +$ 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 +✔ fault switch /admin/fault (day 72 data API) +✔ n8n → http://data-api:8800 +✔ credential 'OpenAI (.env)' (openAiCourse0001) +✔ node catalog (for check) +✔ CLI coding agent: codex, claude, agy +ready +``` + +**쉽게 말하면** + +- 끝나면 `doctor`로 고장 스위치 · OpenAI 키가 평소대로인지 확인합니다. 마지막 줄 `ready`면 됩니다. +- 첫 줄의 `!`는 강사 Mac에 `~/day65-n8n` 폴더가 없어서 나온 경고입니다. 학생 WSL에서는 ✔입니다. + +- 강사 Mac에는 `~/day65-n8n` 폴더가 없어 첫 줄이 `!`입니다 (학생 WSL은 ✔). + +## 다시 돌려 보기 + +```bash +cd ~/aio-01d4-stu/Last3Mon/72 +uv run tools/flow.py push answers/72-lab01-challenge2-budget.json +uv run tools/flow.py test cases/lab01-challenge2.json -v # 4/4 +uv run tools/flow.py push answers/72-lab02-challenge2-faqcache.json +uv run tools/flow.py test cases/lab02-challenge2.json -v # 6/6 +uv run tools/flow.py push reference/lab03-example3-ticket-down.json +uv run tools/flow.py test cases/lab03-ticket.json -v # 4/4 +uv run tools/flow.py push answers/72-lab03-challenge2-breaker.json answers/72-lab03-challenge-health.json +uv run tools/flow.py test cases/lab03-challenge2.json -v # 4/4 +uv run tools/flow.py test cases/lab03-challenge.json -v # 4/4 +uv run tools/flow.py doctor # ready +``` + +- 과제 정답 여섯 파일은 이 폴더(`Last3Mon/72/answers/`)에 있습니다 (강사 저장소 `Day/72/answers/`와 같은 파일). 예제 정답은 `reference/`. +- 고장 연습 뒤에는 `uv run tools/flow.py fault all ok`, `uv run tools/flow.py llm up`, `curl -s -XPOST localhost:8800/cache-clear`(차단기)로 되돌립니다 (테스트 파일은 `after`로 되돌림). diff --git a/Last3Mon/72/tbench/.gitignore b/Last3Mon/72/tbench/.gitignore new file mode 100644 index 0000000..64c75f7 --- /dev/null +++ b/Last3Mon/72/tbench/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.egg-info/ +.pytest_cache/ diff --git a/Last3Mon/72/tbench/Doc/IMP-PLAN.md b/Last3Mon/72/tbench/Doc/IMP-PLAN.md new file mode 100644 index 0000000..98f8de8 --- /dev/null +++ b/Last3Mon/72/tbench/Doc/IMP-PLAN.md @@ -0,0 +1,16 @@ +# tbench IMP-PLAN (강사가 직접 만든 기록) + +## 엔진 1단계 · 부르고 재기 (R1 · R6 · R7 · R8) +- [x] runner.py: call_once → {status, seconds, error_kind, timings}, run() 제너레이터 +- [x] stats.py: nearest-rank (1~10 → p50 5 · p95 10), 실패는 세고 시간 목록에서 뺌, p50 excl. first +- [x] tests/conftest.py 가짜 서버, --plain · --warmup · --header (가리기) +## TUI 1단계 (R2) +- [x] app.py: Sparkline · 요약 · DataTable, worker(thread) + call_from_thread. 테스트: 지연 0.5초 × 6, 1.3초 뒤 1~5줄 → 끝에 6줄 +## 설치 (R14) +- [x] [project.scripts] tbench = "tbench.cli:main", uv tool install --editable ., /tmp 에서 tbench --help +## 엔진 2단계 (R3 · R13 · R11) +- [x] --save (기본 ~/.tbench/results/), compare (url · method · body_sha256 · n · concurrency · warmup 다르면 WARNING), history (최신 위), --log +## 엔진 3단계 (R4 · R10 · R9 · R12) +- [x] --concurrency (0.3초 × 6: k=1 1.8초대, k=3 1초 안), --max-p95 · --max-failures 종료 코드 0 · 1, timings 평균, profile save/list · run +## TUI 2단계 (R5) +- [x] --pick DIR: OptionList, Enter 로 그 본문 실행 diff --git a/Last3Mon/72/tbench/README.md b/Last3Mon/72/tbench/README.md new file mode 100644 index 0000000..ee1f3e4 --- /dev/null +++ b/Last3Mon/72/tbench/README.md @@ -0,0 +1,38 @@ +# tbench · 강사가 수업 준비 때 직접 만든 본 (2026-10-26) + +> **정답 자료** · 학생 저장소에는 수업 뒤 [VIBE-A-TBENCH.md](../VIBE-A-TBENCH.md)와 함께 올립니다. +> 이 본은 강사가 CLI 에이전트에 단계 요청문을 보내 만든 것이 아니라, Claude Code가 VIBE-A의 SPEC · IMP-PLAN을 보고 직접 썼습니다. 학생이 만든 것과 파일 구성이 달라도 됩니다. 요구 R1~R14의 **확인 방법**이 같으면 됩니다. + +## 설치 · 실행 + +```bash +cd ~/aio-01d4-stu/Last3Mon/72/tbench +uv sync && uv run pytest -q # 가짜 서버 테스트 12개 (진짜 헬프데스크를 부르지 않음) +uv tool install --editable . # 어느 폴더에서나 tbench (R14) +cd /tmp && tbench --help +``` + +| 하고 싶은 것 | 명령 | +| --- | --- | +| 화면(TUI)으로 재기 | `tbench http://localhost:5678/webhook/my-helpdesk --body-file requests/faq-vpn-nocache.json -n 20` | +| 글자로 · 저장 · 기준 | `tbench --body-file x.json -n 10 --plain --save --max-p95 2.5 --max-failures 0` | +| 요청 고르기 | `tbench --pick requests` | +| 동시에 · 미리 부르기 | `--concurrency 3` · `--warmup 1` | +| 키 머리글 | `--header 'Authorization: Bearer $MY_KEY'` (저장 · 기록에는 `***`) | +| 비교 · 목록 | `tbench compare a.json b.json` · `tbench history` | +| 설정 저장 · 부르기 | `tbench profile save vpn --body-file requests/faq-vpn-nocache.json -n 10` · `tbench profile list` · `tbench run vpn --plain` | +| 한 번마다 한 줄 | `--log runs.jsonl` | + +저장 위치는 `~/.tbench/` (`results/`, `profiles.toml`). 테스트는 `TBENCH_HOME`을 임시 폴더로 바꿔 씁니다. + +## 파일 + +| 파일 | 하는 일 | +| --- | --- | +| `src/tbench/runner.py` | 엔진: 한 번 부르기(시간 · 상태 · 실패 종류 · `timings`), `run()` 제너레이터 (warmup · 동시) | +| `src/tbench/stats.py` | nearest-rank 백분위, 요약, 기준 검사(PASS · FAIL) | +| `src/tbench/store.py` | 저장(조건 · 본문 해시 · 키 가리기), compare, history, 기록 줄 | +| `src/tbench/profiles.py` | `~/.tbench/profiles.toml` | +| `src/tbench/cli.py` | 명령줄, `--plain` 출력, 종료 코드 | +| `src/tbench/app.py` | Textual 화면 (worker + `call_from_thread`), `--pick` 고르기 | +| `tests/conftest.py` | pytest 안의 가짜 서버 (지연 · N번째마다 500 · 첫 응답만 느림 · 상태 코드 · `timings`) | diff --git a/Last3Mon/72/tbench/pyproject.toml b/Last3Mon/72/tbench/pyproject.toml new file mode 100644 index 0000000..49aae58 --- /dev/null +++ b/Last3Mon/72/tbench/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "tbench" +version = "0.1.0" +description = "Call a webhook many times and show how long each call took" +requires-python = ">=3.11" +dependencies = ["httpx>=0.27", "textual>=0.80"] + +[project.scripts] +tbench = "tbench.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/tbench"] + +[dependency-groups] +dev = ["pytest>=8", "pytest-asyncio>=0.23"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/Last3Mon/72/tbench/src/tbench/__init__.py b/Last3Mon/72/tbench/src/tbench/__init__.py new file mode 100644 index 0000000..b8713a1 --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/__init__.py @@ -0,0 +1 @@ +"""tbench: call a webhook many times and show how long each call took.""" diff --git a/Last3Mon/72/tbench/src/tbench/app.py b/Last3Mon/72/tbench/src/tbench/app.py new file mode 100644 index 0000000..abb9a12 --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/app.py @@ -0,0 +1,75 @@ +"""Textual screen: sparkline and summary on top, one table row per call as it arrives.""" +from pathlib import Path + +from textual import work +from textual.app import App, ComposeResult +from textual.widgets import DataTable, Footer, Header, OptionList, Sparkline, Static +from textual.widgets.option_list import Option + +from .runner import read_body, run +from .stats import fmt, summarize, summary_lines + + +class BenchApp(App): + TITLE = "tbench" + BINDINGS = [("q", "quit", "Quit")] + CSS = """ + Sparkline { height: 4; margin: 0 1; } + #summary { height: auto; margin: 0 1; } + OptionList { height: 1fr; } + """ + + def __init__(self, settings, headers, pick=None): + super().__init__() + self.settings, self.headers, self.pick = settings, headers, pick + self.results, self.body = [], None + + def compose(self) -> ComposeResult: + yield Header() + if self.pick: + files = sorted(Path(self.pick).glob("*.json")) + yield OptionList(*[Option(f.name, id=str(f)) for f in files], id="pick") + yield Sparkline([], id="spark") + yield Static("starting...", id="summary") + yield DataTable(id="calls") + yield Footer() + + def on_mount(self): + table = self.query_one("#calls", DataTable) + table.add_columns("#", "status", "seconds", "note") + if self.pick: + self.sub_title = f"choose a body from {self.pick}" + self.query_one("#summary", Static).update("Enter: run with this body") + else: + self.start(self.settings.get("body_file")) + + def on_option_list_option_selected(self, event): + event.option_list.remove() + self.settings["body_file"] = event.option.id + self.start(event.option.id) + + def start(self, body_file): + s = self.settings + self.body = read_body(body_file) + self.sub_title = f"{s['method']} {s['url']} body={Path(body_file).name if body_file else '-'}" + self.measure() + + @work(thread=True, exclusive=True) + def measure(self): + s = self.settings + for r in run(s["url"], self.body, s["n"], s["method"], self.headers, s["timeout"], s["warmup"], s["concurrency"]): + self.call_from_thread(self.add_result, r) + self.call_from_thread(self.finish) + + def add_result(self, r): + self.results.append(r) + label = f"w{r['i']}" if r["warmup"] else str(r["i"]) + status = str(r["status"]) if r["status"] is not None else r["error_kind"] + note = " ".join(x for x in (r["note"], r["error_kind"] if r["status"] else "") if x) + self.query_one("#calls", DataTable).add_row(label, status, fmt(r["seconds"]), note) + measured = [x for x in self.results if not x["warmup"]] + self.query_one("#spark", Sparkline).data = [x["seconds"] for x in measured if x["error_kind"] is None] + self.query_one("#summary", Static).update("\n".join(summary_lines(summarize(measured, self.settings["n"])))) + + def finish(self): + self.sub_title += " (done, q to quit)" diff --git a/Last3Mon/72/tbench/src/tbench/cli.py b/Last3Mon/72/tbench/src/tbench/cli.py new file mode 100644 index 0000000..56273de --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/cli.py @@ -0,0 +1,166 @@ +"""Command line: `tbench ...` (TUI, or --plain), compare, history, profile, run.""" +import argparse +import os +import sys +from datetime import datetime +from pathlib import Path + +from . import profiles, store +from .runner import read_body, run +from .stats import check, fmt, summarize, summary_lines + +COMMANDS = ("compare", "history", "profile", "run") + + +def parse_headers(items): + headers = {} + for item in items or []: + if ":" not in item: + raise SystemExit(f"bad --header {item!r}, use 'Name: value'") + k, v = item.split(":", 1) + headers[k.strip()] = os.path.expandvars(v.strip()) + return headers + + +def add_run_options(p, url=True): + if url: + p.add_argument("url") + p.add_argument("--body-file", help="JSON file to send as the body") + p.add_argument("-n", type=int, default=None, help="measured calls (default 10)") + p.add_argument("--method", default=None, help="HTTP method (default POST)") + p.add_argument("--timeout", type=float, default=None, help="seconds before a call counts as timeout (default 30)") + p.add_argument("--header", action="append", default=[], help="'Name: value', may repeat; $VARS are expanded") + p.add_argument("--warmup", type=int, default=None, help="calls to make and throw away first") + p.add_argument("--concurrency", type=int, default=None, help="calls in flight at once (default 1)") + p.add_argument("--plain", action="store_true", help="print lines instead of the TUI") + p.add_argument("--save", nargs="?", const="", default=None, help="save result JSON (default ~/.tbench/results/)") + p.add_argument("--log", help="append one JSON line per call to this file") + p.add_argument("--max-p95", type=float, default=None, help="FAIL (exit 1) if p95 seconds is above this") + p.add_argument("--max-failures", type=int, default=None, help="FAIL (exit 1) if failures are above this") + p.add_argument("--pick", metavar="DIR", help="choose the body from *.json in DIR (TUI)") + + +def settings_from(args, base=None): + s = dict(base or {}) + for key in ("url", "body_file", "n", "method", "timeout", "warmup", "concurrency", "max_p95", "max_failures"): + value = getattr(args, key, None) + if value is not None: + s[key] = value + if getattr(args, "header", None): + s["headers"] = args.header + s.setdefault("n", 10) + s.setdefault("method", "POST") + s.setdefault("timeout", 30.0) + s.setdefault("warmup", 0) + s.setdefault("concurrency", 1) + return s + + +def execute(s, plain, save_to, log_path, pick=None): + headers = parse_headers(s.get("headers")) + started = datetime.now().astimezone().isoformat(timespec="seconds") + if not plain: + from .app import BenchApp + app = BenchApp(s, headers, pick=pick) + app.run() + results, body = app.results, app.body + if not results: + return 0 + else: + body = read_body(s.get("body_file")) + results = [] + log = open(log_path, "a", encoding="utf-8") if log_path else None + print(f"{s['method']} {s['url']} n={s['n']} concurrency={s['concurrency']} warmup={s['warmup']}") + try: + for r in run(s["url"], body, s["n"], s["method"], headers, s["timeout"], s["warmup"], s["concurrency"]): + results.append(r) + if log: + store.log_line(log, r) + label = f"w{r['i']}" if r["warmup"] else f"#{r['i']}" + status = r["status"] if r["status"] is not None else r["error_kind"] + note = " ".join(x for x in (r["note"], r["error_kind"] if r["status"] else "") if x) + print(f"{label:>4} {status!s:>8} {fmt(r['seconds']):>8}s {note}".rstrip()) + finally: + if log: + log.close() + if log_path and not plain: + with open(log_path, "a", encoding="utf-8") as log: + for r in results: + store.log_line(log, r) + measured = [r for r in results if not r["warmup"]] + summary = summarize(measured, s["n"]) + for line in summary_lines(summary): + print(line) + if save_to is not None: + record = store.build_record(s["url"], s["method"], body, headers, s["n"], s["concurrency"], + s["warmup"], started, results) + print(f"saved {store.save(record, save_to or None)}") + if s.get("max_p95") is not None or s.get("max_failures") is not None: + passed, line = check(summary, s.get("max_p95"), s.get("max_failures")) + print(line) + return 0 if passed else 1 + return 0 + + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + if argv and argv[0] in COMMANDS: + return sys.exit(sub_main(argv)) + p = argparse.ArgumentParser(prog="tbench", description="Call a URL many times and show how long each call took. " + "Also: tbench compare A B | history | profile save/list | run NAME") + add_run_options(p) + args = p.parse_args(argv) + sys.exit(execute(settings_from(args), args.plain, args.save, args.log, args.pick)) + + +def sub_main(argv): + p = argparse.ArgumentParser(prog="tbench") + sub = p.add_subparsers(dest="cmd", required=True) + c = sub.add_parser("compare", help="compare two saved results") + c.add_argument("a") + c.add_argument("b") + sub.add_parser("history", help="list saved results, newest first") + prof = sub.add_parser("profile", help="save or list named settings") + psub = prof.add_subparsers(dest="action", required=True) + ps = psub.add_parser("save") + ps.add_argument("name") + add_run_options(ps) + psub.add_parser("list") + r = sub.add_parser("run", help="run a saved profile") + r.add_argument("name") + add_run_options(r, url=False) + args = p.parse_args(argv) + + if args.cmd == "compare": + for line in store.compare(store.load(args.a), store.load(args.b)): + print(line) + return 0 + if args.cmd == "history": + rows = store.history() + print(f"{'started':19} {'n':>3} {'p50':>7} {'p95':>7} {'fail':>4} url") + for started, url, n, p50, p95, failures, _ in rows: + print(f"{started:19} {n:>3} {p50:>7} {p95:>7} {failures:>4} {url}") + return 0 + if args.cmd == "profile" and args.action == "list": + for name, values in profiles.load_all().items(): + print(f"{name:12} {values.get('url')} body={values.get('body_file', '-')} n={values.get('n', 10)}") + return 0 + if args.cmd == "profile": + s = {k: v for k, v in vars(args).items() if k in profiles.FIELDS and v is not None} + if s.get("body_file"): + s["body_file"] = str(Path(s["body_file"]).resolve()) + safe = [] + for h in args.header: + name, _, value = h.partition(":") + if store.mask_headers({name: 1})[name] == "***" and "$" not in value: + print(f"note: {name.strip()} is kept as *** — pass it as an env var, e.g. 'Authorization: Bearer $MY_KEY'") + value = " ***" + safe.append(f"{name}:{value}") + s["headers"] = safe + print(f"saved {args.name} to {profiles.save(args.name, s)}") + return 0 + base = profiles.load_all().get(args.name) + if base is None: + print(f"no profile {args.name!r} in {profiles.path()}") + return 2 + return execute(settings_from(args, base), args.plain, args.save, args.log, args.pick) diff --git a/Last3Mon/72/tbench/src/tbench/profiles.py b/Last3Mon/72/tbench/src/tbench/profiles.py new file mode 100644 index 0000000..57b7582 --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/profiles.py @@ -0,0 +1,37 @@ +"""Named settings in ~/.tbench/profiles.toml, so `tbench run ` works from any folder.""" +import tomllib + +from .store import home + +FIELDS = ("url", "body_file", "n", "method", "timeout", "warmup", "concurrency", "headers", "max_p95", "max_failures") + + +def path(): + return home() / "profiles.toml" + + +def load_all(): + p = path() + return tomllib.loads(p.read_text(encoding="utf-8")) if p.exists() else {} + + +def _value(v): + if isinstance(v, str): + return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' + if isinstance(v, list): + return "[" + ", ".join(_value(x) for x in v) + "]" + return str(v).lower() if isinstance(v, bool) else str(v) + + +def save(name, settings): + profiles = load_all() + profiles[name] = {k: v for k, v in settings.items() if k in FIELDS and v not in (None, [])} + out = [] + for pname, values in profiles.items(): + out.append(f'["{pname}"]') + out += [f"{k} = {_value(v)}" for k, v in values.items()] + out.append("") + p = path() + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text("\n".join(out), encoding="utf-8") + return p diff --git a/Last3Mon/72/tbench/src/tbench/runner.py b/Last3Mon/72/tbench/src/tbench/runner.py new file mode 100644 index 0000000..8abe93d --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/runner.py @@ -0,0 +1,67 @@ +"""Call a URL many times. run() yields one result dict per call, as it finishes.""" +import json +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import httpx + + +def call_once(client, url, method, body, headers, timeout): + """One call, timed from just before sending until the whole body is read.""" + start = time.perf_counter() + status, error_kind, timings = None, None, None + try: + response = client.request(method, url, content=body, headers=headers, timeout=timeout) + response.read() + status = response.status_code + if status >= 500: + error_kind = "http_5xx" + elif status >= 400: + error_kind = "http_4xx" + else: + try: + data = response.json() + if isinstance(data, dict) and isinstance(data.get("timings"), dict): + timings = data["timings"] + except ValueError: + pass + except httpx.TimeoutException: + error_kind = "timeout" + except httpx.TransportError: + error_kind = "connect" + return {"status": status, "seconds": round(time.perf_counter() - start, 4), + "error_kind": error_kind, "timings": timings} + + +def run(url, body=None, n=10, method="POST", headers=None, timeout=30.0, warmup=0, concurrency=1): + """Yield warmup calls (warmup=True) first, then n measured calls numbered 1..n.""" + headers = dict(headers or {}) + if body is not None and not any(k.lower() == "content-type" for k in headers): + headers["Content-Type"] = "application/json" + content = body.encode() if isinstance(body, str) else body + with httpx.Client() as client: + for w in range(warmup): + r = call_once(client, url, method, content, headers, timeout) + yield {"i": w + 1, "warmup": True, "note": "warmup", **r} + first_note = "first call" if warmup == 0 else "" + if concurrency <= 1: + for i in range(1, n + 1): + r = call_once(client, url, method, content, headers, timeout) + yield {"i": i, "warmup": False, "note": first_note if i == 1 else "", **r} + return + with ThreadPoolExecutor(max_workers=concurrency) as pool: + futures = {pool.submit(call_once, client, url, method, content, headers, timeout): i + for i in range(1, n + 1)} + for future in as_completed(futures): + i = futures[future] + yield {"i": i, "warmup": False, "note": first_note if i == 1 else "", **future.result()} + + +def read_body(path): + """Read a JSON body file; keep the exact text so its hash matches what was sent.""" + if path is None: + return None + with open(path, encoding="utf-8") as f: + text = f.read() + json.loads(text) + return text diff --git a/Last3Mon/72/tbench/src/tbench/stats.py b/Last3Mon/72/tbench/src/tbench/stats.py new file mode 100644 index 0000000..51da7a9 --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/stats.py @@ -0,0 +1,69 @@ +"""Percentiles and summaries. Nearest-rank: sort, take the ceil(p * n)-th value.""" +import math + +KINDS = ("connect", "timeout", "http_5xx", "http_4xx") + + +def percentile(values, p): + if not values: + return None + ordered = sorted(values) + rank = max(1, math.ceil(p * len(ordered) - 1e-9)) + return ordered[rank - 1] + + +def average_timings(results): + """Average every numeric field of response `timings` over successful calls.""" + sums, counts = {}, {} + for r in results: + for key, value in (r.get("timings") or {}).items(): + if isinstance(value, (int, float)) and not isinstance(value, bool): + sums[key] = sums.get(key, 0) + value + counts[key] = counts.get(key, 0) + 1 + return {k: round(sums[k] / counts[k], 1) for k in sums} + + +def summarize(results, total=None): + """results: measured calls (no warmup). Failures are counted, not timed.""" + ok = [r for r in results if r["error_kind"] is None] + times = [r["seconds"] for r in ok] + rest = [r["seconds"] for r in ok if r.get("note") != "first call"] + by_kind = {k: sum(1 for r in results if r["error_kind"] == k) for k in KINDS} + return { + "done": len(results), + "total": total if total is not None else len(results), + "ok": len(ok), + "failures": len(results) - len(ok), + "by_kind": by_kind, + "p50": percentile(times, 0.5), + "p95": percentile(times, 0.95), + "max": max(times) if times else None, + "p50_excl_first": percentile(rest, 0.5) if len(rest) < len(times) else None, + "timings": average_timings(ok), + } + + +def fmt(seconds): + return "-" if seconds is None else f"{seconds:.3f}" + + +def summary_lines(s): + kinds = " ".join(f"{k}={v}" for k, v in s["by_kind"].items() if v) + lines = [ + f"done {s['done']}/{s['total']} failures {s['failures']}" + (f" ({kinds})" if kinds else ""), + f"p50 {fmt(s['p50'])}s p95 {fmt(s['p95'])}s max {fmt(s['max'])}s" + + (f" p50 excl. first {fmt(s['p50_excl_first'])}s" if s["p50_excl_first"] is not None else ""), + ] + if s["timings"]: + lines.append("timings avg ms " + " ".join(f"{k} {v:g}" for k, v in s["timings"].items())) + return lines + + +def check(s, max_p95=None, max_failures=None): + """Return (passed, line) for the given limits.""" + problems = [] + if max_p95 is not None and (s["p95"] is None or s["p95"] > max_p95): + problems.append(f"p95 {fmt(s['p95'])} > {max_p95}") + if max_failures is not None and s["failures"] > max_failures: + problems.append(f"failures {s['failures']} > {max_failures}") + return (not problems, "FAIL " + ", ".join(problems) if problems else "PASS") diff --git a/Last3Mon/72/tbench/src/tbench/store.py b/Last3Mon/72/tbench/src/tbench/store.py new file mode 100644 index 0000000..7b5a7d7 --- /dev/null +++ b/Last3Mon/72/tbench/src/tbench/store.py @@ -0,0 +1,101 @@ +"""Save, compare and list results; per-call log lines; header masking. Everything lives under ~/.tbench.""" +import hashlib +import json +import os +import time +from datetime import datetime +from pathlib import Path + +from .stats import fmt, summarize + +SECRET_WORDS = ("authorization", "key", "token", "secret", "cookie", "password") + + +def home(): + return Path(os.environ.get("TBENCH_HOME", Path.home() / ".tbench")) + + +def mask_headers(headers): + return {k: ("***" if any(w in k.lower() for w in SECRET_WORDS) else v) for k, v in (headers or {}).items()} + + +def body_sha256(body): + return hashlib.sha256((body or "").encode()).hexdigest() + + +def build_record(url, method, body, headers, n, concurrency, warmup, started, results): + measured = sorted((r for r in results if not r["warmup"]), key=lambda r: r["i"]) + return { + "url": url, "method": method, "headers": mask_headers(headers), + "body_sha256": body_sha256(body), "n": n, "concurrency": concurrency, "warmup": warmup, + "started": started, + "seconds": [r["seconds"] for r in measured if r["error_kind"] is None], + "statuses": [r["status"] for r in measured], + "errors": [r["error_kind"] for r in measured], + "calls": [{k: r[k] for k in ("i", "status", "seconds", "error_kind", "note")} for r in measured], + "summary": summarize(measured, n), + } + + +def save(record, path=None): + if path is None: + folder = home() / "results" + folder.mkdir(parents=True, exist_ok=True) + stamp = datetime.fromisoformat(record["started"]).strftime("%Y%m%d-%H%M%S") + path = folder / f"{stamp}.json" + k = 1 + while path.exists(): + k += 1 + path = folder / f"{stamp}-{k}.json" + path = Path(path) + path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8") + return path + + +def load(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def log_line(fh, result): + row = {"t": time.time(), "i": result["i"], "status": result["status"], + "seconds": result["seconds"], "error_kind": result["error_kind"]} + if result["warmup"]: + row["warmup"] = True + fh.write(json.dumps(row) + "\n") + fh.flush() + + +def _change(a, b): + if a is None or b is None: + return "-" + if a == 0: + return "-" if b == 0 else "new" + return f"{(b - a) / a * 100:+.1f}%" + + +def compare(a, b): + """Return lines: warnings first, then a table of p50 · p95 · max · failures.""" + lines = [] + for key in ("url", "method", "body_sha256", "n", "concurrency", "warmup"): + if a.get(key) != b.get(key): + lines.append(f"WARNING: different {key}: {a.get(key)} vs {b.get(key)}") + sa, sb = a["summary"], b["summary"] + lines.append(f"{'':10}{'A':>10}{'B':>10}{'change':>10}") + for key in ("p50", "p95", "max"): + lines.append(f"{key:10}{fmt(sa[key]):>10}{fmt(sb[key]):>10}{_change(sa[key], sb[key]):>10}") + lines.append(f"{'failures':10}{sa['failures']:>10}{sb['failures']:>10}{_change(sa['failures'], sb['failures']):>10}") + return lines + + +def history(): + folder = home() / "results" + rows = [] + for path in sorted(folder.glob("*.json"), reverse=True) if folder.exists() else []: + try: + r = load(path) + except (ValueError, OSError): + continue + s = r["summary"] + rows.append((r["started"][:19].replace("T", " "), r["url"], r["n"], fmt(s["p50"]), fmt(s["p95"]), s["failures"], path.name)) + rows.sort(key=lambda row: row[0], reverse=True) + return rows diff --git a/Last3Mon/72/tbench/tests/conftest.py b/Last3Mon/72/tbench/tests/conftest.py new file mode 100644 index 0000000..14e43ae --- /dev/null +++ b/Last3Mon/72/tbench/tests/conftest.py @@ -0,0 +1,68 @@ +"""A fake server inside pytest, so tests never call the real helpdesk (or OpenAI). + +Query options: delay=, fail_every= (every k-th call answers 500), first_delay=, +status=, timings=1 (answer with a helpdesk-like `timings`). Counters reset per test. +""" +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlparse + +import pytest + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_POST(self): + length = int(self.headers.get("Content-Length") or 0) + self.server.bodies.append(self.rfile.read(length).decode()) + self.handle_call() + + def do_GET(self): + self.handle_call() + + def handle_call(self): + q = {k: v[0] for k, v in parse_qs(urlparse(self.path).query).items()} + with self.server.lock: + self.server.count += 1 + count = self.server.count + delay = float(q.get("first_delay", 0)) if count == 1 and "first_delay" in q else float(q.get("delay", 0)) + time.sleep(delay) + status = int(q.get("status", 200)) + if "fail_every" in q and count % int(q["fail_every"]) == 0: + status = 500 + payload = {"count": count} + if q.get("timings"): + payload["timings"] = {"classify_ms": 100 * count, "answer_ms": 300, "total_ms": 400 + 100 * count} + data = json.dumps(payload).encode() + try: + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + except (BrokenPipeError, ConnectionResetError): + pass + + +@pytest.fixture +def server(tmp_path, monkeypatch): + monkeypatch.setenv("TBENCH_HOME", str(tmp_path / "home")) + httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + httpd.daemon_threads = True + httpd.count, httpd.lock, httpd.bodies = 0, threading.Lock(), [] + threading.Thread(target=httpd.serve_forever, daemon=True).start() + httpd.base = f"http://127.0.0.1:{httpd.server_address[1]}/hook" + yield httpd + httpd.shutdown() + httpd.server_close() + + +@pytest.fixture +def body_file(tmp_path): + p = tmp_path / "faq-vpn.json" + p.write_text(json.dumps({"question": "VPN 접속이 자꾸 끊겨요", "no_cache": True}, ensure_ascii=False)) + return p diff --git a/Last3Mon/72/tbench/tests/test_app.py b/Last3Mon/72/tbench/tests/test_app.py new file mode 100644 index 0000000..6958a69 --- /dev/null +++ b/Last3Mon/72/tbench/tests/test_app.py @@ -0,0 +1,34 @@ +"""R2 · R5: rows appear while calls are still running; picking a body file starts the run.""" +import asyncio + +from textual.widgets import DataTable, OptionList + +from tbench.app import BenchApp +from tbench.cli import settings_from + + +def settings(url, n, body_file=None): + return settings_from(type("A", (), {"url": url, "n": n, "body_file": body_file})()) + + +async def test_rows_arrive_one_by_one(server, body_file): + app = BenchApp(settings(server.base + "?delay=0.5", 6, str(body_file)), {}) + async with app.run_test() as pilot: + await asyncio.sleep(1.3) + partial = app.query_one("#calls", DataTable).row_count + assert 1 <= partial < 6 + await asyncio.sleep(2.5) + await pilot.pause() + assert app.query_one("#calls", DataTable).row_count == 6 + assert "done 6/6" in str(app.query_one("#summary").render()) + + +async def test_pick_a_body(server, body_file): + app = BenchApp(settings(server.base, 2), {}, pick=str(body_file.parent)) + async with app.run_test() as pilot: + assert app.query_one(OptionList).option_count == 1 + await pilot.press("enter") + await asyncio.sleep(0.5) + await pilot.pause() + assert app.query_one("#calls", DataTable).row_count == 2 + assert server.bodies diff --git a/Last3Mon/72/tbench/tests/test_engine.py b/Last3Mon/72/tbench/tests/test_engine.py new file mode 100644 index 0000000..d27418d --- /dev/null +++ b/Last3Mon/72/tbench/tests/test_engine.py @@ -0,0 +1,129 @@ +import json +import socket +import time + +import pytest + +from tbench import cli, profiles, store +from tbench.runner import run +from tbench.stats import check, percentile, summarize + + +def free_port(): + s = socket.socket() + s.bind(("127.0.0.1", 0)) + port = s.getsockname()[1] + s.close() + return port + + +# R1 · nearest-rank fixed by a test +def test_percentile_nearest_rank(): + values = list(range(1, 11)) + assert percentile(values, 0.5) == 5 + assert percentile(values, 0.95) == 10 + assert percentile([3.0], 0.95) == 3.0 + assert percentile([], 0.5) is None + + +def test_failures_are_counted_not_timed(server, body_file): + results = list(run(server.base + "?delay=0.1&fail_every=3", body_file.read_text(), n=6)) + s = summarize(results, 6) + assert s["failures"] == 2 and s["by_kind"]["http_5xx"] == 2 + assert s["ok"] == 4 and len(results) == 6 + + +def test_body_is_sent(server, body_file): + list(run(server.base, body_file.read_text(), n=1)) + assert json.loads(server.bodies[0])["question"].startswith("VPN") + + +# R6 +def test_first_call_and_warmup(server): + results = list(run(server.base + "?first_delay=1&delay=0.05", None, n=4, method="GET")) + assert results[0]["note"] == "first call" + s = summarize(results, 4) + assert s["max"] >= 1 and s["p50_excl_first"] < 0.5 + server.count = 0 + results = list(run(server.base + "?first_delay=1&delay=0.05", None, n=4, method="GET", warmup=1)) + measured = [r for r in results if not r["warmup"]] + assert len(measured) == 4 and max(r["seconds"] for r in measured) < 0.5 + assert all(r["note"] != "first call" for r in measured) + + +# R7 +def test_error_kinds(server): + assert list(run(f"http://127.0.0.1:{free_port()}/x", None, n=1))[0]["error_kind"] == "connect" + assert list(run(server.base + "?delay=3", None, n=1, timeout=1))[0]["error_kind"] == "timeout" + assert list(run(server.base + "?status=500", None, n=1))[0]["error_kind"] == "http_5xx" + assert list(run(server.base + "?status=404", None, n=1))[0]["error_kind"] == "http_4xx" + + +# R8 · R3 · R11 · R13 +def test_save_masks_keys_compare_warns_history_and_log(server, body_file, tmp_path, capsys): + out_a, out_b, log = tmp_path / "a.json", tmp_path / "b.json", tmp_path / "runs.jsonl" + url = server.base + "?delay=0.05" + cli.execute(cli.settings_from(type("A", (), {"url": url, "body_file": str(body_file), "n": 3, + "header": ["Authorization: Bearer sk-test", "X-Api-Key: sk-test2"]})()), + True, str(out_a), str(log)) + other = tmp_path / "other.json" + other.write_text('{"question": "점심 메뉴"}') + cli.execute(cli.settings_from(type("A", (), {"url": url, "body_file": str(other), "n": 3})()), True, str(out_b), None) + assert "sk-test" not in out_a.read_text() and "sk-test" not in log.read_text() + assert json.loads(out_a.read_text())["headers"]["Authorization"] == "***" + assert len(log.read_text().splitlines()) == 3 + lines = store.compare(store.load(out_a), store.load(out_b)) + assert any(line.startswith("WARNING: different body_sha256") for line in lines) + assert not any("WARNING" in line for line in store.compare(store.load(out_a), store.load(out_a))) + + cli.execute(cli.settings_from(type("A", (), {"url": url, "n": 2})()), True, "", None) + time.sleep(1.1) + cli.execute(cli.settings_from(type("A", (), {"url": url, "n": 4})()), True, "", None) + rows = store.history() + assert [r[2] for r in rows] == [4, 2] + + +# R4 +def test_concurrency_is_faster(server): + t = time.perf_counter() + list(run(server.base + "?delay=0.3", None, n=6, concurrency=1)) + one = time.perf_counter() - t + t = time.perf_counter() + results = list(run(server.base + "?delay=0.3", None, n=6, concurrency=3)) + three = time.perf_counter() - t + assert one > 1.7 and three < 1.0 + assert sorted(r["i"] for r in results) == [1, 2, 3, 4, 5, 6] + + +# R10 +def test_limits_and_exit_code(server): + s = summarize(list(run(server.base + "?delay=0.2", None, n=3)), 3) + assert check(s, max_p95=0.1) == (False, f"FAIL p95 {s['p95']:.3f} > 0.1") + assert check(s, max_p95=5, max_failures=0) == (True, "PASS") + with pytest.raises(SystemExit) as e: + cli.main([server.base + "?status=500", "-n", "2", "--plain", "--max-failures", "0"]) + assert e.value.code == 1 + with pytest.raises(SystemExit) as e: + cli.main([server.base, "-n", "2", "--plain", "--max-p95", "5"]) + assert e.value.code == 0 + + +# R9 +def test_timings_average(server): + s = summarize(list(run(server.base + "?timings=1", None, n=2)), 2) + assert s["timings"] == {"classify_ms": 150.0, "answer_ms": 300.0, "total_ms": 550.0} + assert summarize(list(run(server.base, None, n=1)), 1)["timings"] == {} + + +# R12 +def test_profile_save_and_run_from_another_folder(server, body_file, tmp_path, monkeypatch, capsys): + with pytest.raises(SystemExit): + cli.main(["profile", "save", "vpn", server.base, "--body-file", str(body_file), "-n", "3", + "--header", "Authorization: Bearer sk-live"]) + assert "sk-live" not in profiles.path().read_text() + monkeypatch.chdir(tmp_path) + with pytest.raises(SystemExit) as e: + cli.main(["run", "vpn", "--plain"]) + assert e.value.code == 0 + out = capsys.readouterr().out + assert "done 3/3" in out and server.bodies diff --git a/Last3Mon/72/tbench/uv.lock b/Last3Mon/72/tbench/uv.lock new file mode 100644 index 0000000..2f98da6 --- /dev/null +++ b/Last3Mon/72/tbench/uv.lock @@ -0,0 +1,267 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "certifi" +version = "2026.10.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, +] + +[[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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[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.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, +] + +[[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 = "tbench" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "textual" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27" }, + { name = "textual", specifier = ">=0.80" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8" }, + { name = "pytest-asyncio", specifier = ">=0.23" }, +] + +[[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 = "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" }, +]