77일차 실행 기록 분석: 실습 15개 · 강의(개요 30장 · 단계 문서 · 4단계 정리 · 05 바이브), 정답본 LAB-A · answers · VIBE-A · tlog 포함
This commit is contained in:
1 parent
036eae1c6a
commit
dcd4cdd66b
148 files changed
+40344
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
runs/
|
||||||
|
tools/.cache/
|
||||||
|
my-work/*.json
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# AGENTS.md · n8n 실행 로그 분석 작업 규칙 (77일차 실습 폴더)
|
||||||
|
|
||||||
|
이 파일은 **CLI 코딩 에이전트**(Codex, Claude Code, Antigravity 등)가 이 폴더에서 n8n 워크플로우를 만들 때 따르는 규칙입니다. 사람도 읽을 수 있게 썼습니다. 76일차 규칙과 같고, 3절이 오늘 만드는 **로그 보고서 · 요약 · 주간 보고 · SLO 알림 · 라이브 헬프데스크의 계약**과 **지표 정의**, 5절에 로그를 읽고 계산하는 노드가 더해졌습니다.
|
||||||
|
|
||||||
|
## 1. 작업 순서 (항상 이 순서)
|
||||||
|
|
||||||
|
1. 요청을 읽고, 만들 워크플로우의 **입력 · 노드 순서 · 결과 모양**을 3~5줄로 먼저 정리해 보여 준다. 지표 요청이면 **지표 정의(무엇을 세고 무엇으로 나누는지)** 를 먼저 적어 보여 준다.
|
||||||
|
2. 워크플로우 JSON을 `my-work/<이름>.json`에 쓴다. 이미 있으면 그 파일을 고친다 (새 파일을 만들지 않는다).
|
||||||
|
3. `uv run tools/flow.py check my-work/<이름>.json` → 오류(✘)가 없을 때까지 고친다.
|
||||||
|
4. 끝나면 한국어로 짧게 보고한다: 바꾼 노드, check 결과, 그리고 사람이 실행할 명령 두 줄.
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/<이름>.json
|
||||||
|
uv run tools/flow.py test cases/<요청에 나온 테스트 파일>.json
|
||||||
|
```
|
||||||
|
5. **push와 test는 사람이 실행해 결과를 확인한다.** 사람이 "네가 push·test까지 해"라고 하면 직접 실행해도 된다.
|
||||||
|
6. 사람이 실패 보고서(`runs/*.json`)를 알려 주면 실패한 케이스의 요청·응답을 읽고 원인을 설명한 뒤 고친다. **같은 곳을 3번 고쳐도 실패하면 멈추고** 다른 방법을 제안한다.
|
||||||
|
7. 문서(`my-work/*.md`)를 써 달라는 요청이면 워크플로우를 만들지 않고 문서만 쓴다.
|
||||||
|
|
||||||
|
## 2. 환경 (바꾸지 말 것)
|
||||||
|
|
||||||
|
| 항목 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| n8n | 2.42.6, 65일차 공식 설치 `~/day65-n8n` (Docker 프로젝트 `day65-n8n`), 브라우저 http://localhost:5678 |
|
||||||
|
| n8n 켜고 끄기 | `~/aio-01d4-stu/Last3Mon/65/n8n`에서 `docker compose start/stop` (사람이 함, Ollama 컨테이너는 없음) |
|
||||||
|
| 게시된 웹훅 주소 | 밖에서 · n8n 안에서 모두 `http://localhost:5678/webhook/<path>` |
|
||||||
|
| OpenAI Credential | id `openAiCourse0001`, name `OpenAI (.env)`, 모델 `gpt-4.1-mini` |
|
||||||
|
| 다른 Credential | 쓰지 않음 (Ollama 등 없음) |
|
||||||
|
| 데이터 API | n8n 안에서 `http://data-api:8800`, 터미널에서 `http://localhost:8800` (**77일차 판** `Last3Mon/77/data-api`: 76일차 + 실행 로그 + FAQ 더하기) |
|
||||||
|
| 비교 기준 · 제공 | `reference/baseline-report.json` (`base-report`, 팀이 지난달 급히 만든 보고서 — 정의가 다름, 고치지 않는다) · `reference/helpdesk-live.json` (`my-helpdesk`, 실행마다 로그를 남기는 헬프데스크 — Lab 03에서 복사해 고침) |
|
||||||
|
|
||||||
|
### 데이터 API (77일차 판)
|
||||||
|
|
||||||
|
| 요청 | 응답 |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /logs/query` 본문 `{"from": "2026-10-26", "to": "2026-10-30", "tag": "", "source": "all"}` | `{"count", "logs": [...]}` — 날짜는 **포함**(`from ≤ 날짜 ≤ to`), 비우면 전체. `source`: `all` · `seed`(두 주 기록) · `live`(오늘 실습) |
|
||||||
|
| `GET /logs?from=&to=&tag=&source=&limit=` | 같음 |
|
||||||
|
| `POST /logs` 본문 `{"items": [{...}]}` | 실행 기록 더하기 (`started_at`이 없으면 지금) |
|
||||||
|
| `POST /logs-reset` | 오늘 실습 기록(`live`)만 지움 (두 주 기록은 그대로) |
|
||||||
|
| `POST /faq/add` 본문 `{"id", "title", "keywords": [...], "answer"}` · `POST /faq-reset` | FAQ 더하기 · 되돌리기 (**사람이**) |
|
||||||
|
| `GET /faq-extra` | 오늘 더한 FAQ `{"count", "faq"}` |
|
||||||
|
| `POST /alerts` · `POST /alerts/list` · `POST /alerts/latest` | 알림 남기기 · 거르기 `{"workflow", "reason", "within": 초}` · 가장 최근 (76일차와 같음) |
|
||||||
|
| `POST /admin/fault` | `{"faq": "slow", "slow_seconds": 1.2}` 등 (72일차와 같음, **사람이** · 테스트가) |
|
||||||
|
|
||||||
|
`uv run tools/flow.py logs [--from --to --tag --source --last N]`가 기록을 훑어보고, `ops`가 오늘 기록 · 더한 FAQ · 알림을, `ops --reset`이 그것을 비웁니다 (**사람이**).
|
||||||
|
|
||||||
|
### 실행 기록 한 줄
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"execution_id": 5301, "workflow": "helpdesk", "started_at": "2026-10-26T09:05:39", "user": "윤하늘", "topic": "비밀번호",
|
||||||
|
"question": "...", "release": "1.0.0", "channel": "stable", "status": "success" | "error", "error_node": null, "error_message": null,
|
||||||
|
"route": "자동 답변" | "담당자 연결" | "잠시 후" | null, "duration_ms": 1164, "steps": {"faq_ms": 20, "answer_ms": 1100},
|
||||||
|
"llm_calls": 1, "cost_usd": 0.00012, "repeat": false, "tag": ""}
|
||||||
|
```
|
||||||
|
- `route`가 `null`이면 실행이 오류로 끝난 것. `repeat`: 같은 사람이 10분 안에 같은 주제를 다시 물음 (답이 도움이 안 됐다는 신호).
|
||||||
|
- `steps`: 단계마다 걸린 시간. 그 단계를 거치지 않은 실행은 0 (`잠시 후`는 FAQ · 답 모두 0, `담당자 연결`은 답 0).
|
||||||
|
|
||||||
|
## 3. 오늘 만드는 것의 계약과 지표 정의
|
||||||
|
|
||||||
|
| 무엇 | 파일 · 웹훅 | 입력 (본문) | 결과 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 로그 보고서 | `my-work/log-report.json` · `POST /webhook/my-log-report` | `from`, `to`, `tag`, `source`, `slo`(선택) | 아래 지표 |
|
||||||
|
| 로그 요약 | `my-work/log-summary.json` · `POST /webhook/my-log-summary` | 보고서와 같음 | `{"summary", "numbers_ok", "unknown_numbers", "top_topic", "worst_hour"}` |
|
||||||
|
| 주간 보고 | `my-work/weekly.json` · `POST /webhook/my-weekly` | `from`, `to`, `prev_from`, `prev_to` | `{"markdown", "total", "error_rate", "slo_ok", "numbers_ok"}` |
|
||||||
|
| SLO 알림 (Lab 03 과제 2) | `my-work/slo-alert.json` · `POST /webhook/my-slo-alert` | 보고서와 같음 + `slo` | `{"ok", "breaches", "alert": "sent" \| "suppressed" \| "none", "message", "total"}` + 보낼 때 `POST /alerts` |
|
||||||
|
| 라이브 헬프데스크 | `my-work/helpdesk.json` · `POST /webhook/my-helpdesk` (제공 파일을 복사) | `question`, `user`, `tag` | `{"route", "status", "topic", "answer", "error"}` + `/logs`에 한 줄 |
|
||||||
|
|
||||||
|
| 지표 | 정의 |
|
||||||
|
| --- | --- |
|
||||||
|
| `total` · `errors` | 실행 수 · `status`가 `error`인 수 |
|
||||||
|
| `error_rate` | `errors / total`, 소수 4자리 반올림 (0건이면 0) |
|
||||||
|
| `p50_ms` · `p95_ms` | `duration_ms`를 오름차순으로 놓고 **`ceil(p × n)`번째** 값 (n = 실행 수, 오류 포함) |
|
||||||
|
| `by_route` | `route`별 수, `null`은 `"오류"` |
|
||||||
|
| `by_day` | `{"2026-10-26": {"total", "errors", "error_rate"}}`, 날짜순 |
|
||||||
|
| `by_release` | `route`가 `자동 답변`인 실행만: `{"1.2.0": {"answers", "repeats", "repeat_rate"}}` |
|
||||||
|
| `steps` (Lab 01 과제 2) | `{"faq_ms": {"runs", "p50", "p95"}, "answer_ms": {...}}` — 그 단계 값이 0보다 큰 실행만, 백분위 정의는 위와 같음 |
|
||||||
|
| `cost_usd` · `cost_per_run` · `llm_calls` | 비용 합(소수 5자리) · 실행당 비용(`cost_usd / total`, 소수 6자리, 0건이면 0) · LLM 호출 수 합 |
|
||||||
|
| `failures` | 오류를 `(error_node, error_message)`로 묶어 `[{"node", "message", "count"}]`, 많은 순 |
|
||||||
|
| `unanswered_topics` | `route`가 `담당자 연결`인 실행을 `topic`으로 묶어 `[{"topic", "count"}]`, 많은 순 |
|
||||||
|
| `repeat_topics` (Lab 02 과제 1) | `자동 답변`인 실행을 `topic`으로 묶어 `[{"topic", "answers", "repeats", "repeat_rate"}]`, 다시 물음이 0인 주제는 빼고, `repeat_rate` 높은 순(같으면 `repeats` 많은 순) |
|
||||||
|
| `anomalies` | 시간(`started_at` 앞 13자, 예 `2026-10-26T09`)별로 묶어 **실행 5건 이상**이고 **오류율 ≥ 전체 오류율의 3배**이고 오류가 1건 이상인 시간 `[{"hour", "total", "errors", "error_rate"}]`, 시간순 |
|
||||||
|
| `unanswered_rate` | `담당자 연결` 수 / `total` |
|
||||||
|
| `slo` | 기준(기본 `error_rate` 0.01 · `p95_ms` 3000 · `unanswered_rate` 0.02, 본문 `slo`로 바꿀 수 있음)을 넘은 지표 이름 `breaches`, `ok`, `limits` |
|
||||||
|
|
||||||
|
- 비율은 모두 소수 4자리 반올림, 분모가 0이면 0. 날짜는 **포함**(`from ≤ started_at 앞 10자 ≤ to`, 데이터 API가 거름).
|
||||||
|
- 요약의 숫자 확인: 요약 글의 숫자가 모두 통계 안에 있는지(비율은 ×100 반올림도 허용, 날짜 · 시간의 숫자 허용) **Code가** 확인해 `numbers_ok`. 요약은 `my-log-report`를 **다시 불러** 받은 통계로 쓴다 (정의를 두 벌 만들지 않는다).
|
||||||
|
- SLO 알림: `breaches`가 없으면 `none`. 있으면 `POST /alerts/list {"workflow": "slo", "reason": "slo", "within": 600}`로 10분 안의 같은 알림을 보고, 있으면 `suppressed`, 없으면 `POST /alerts {"workflow": "slo", "severity": "high", "reason": "slo", "breaches", "message"}` 후 `sent`. `message`는 `SLO breach: error_rate 0.0588 > 0.01, …`.
|
||||||
|
|
||||||
|
## 4. 워크플로우 파일 규칙
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"name": "77 Log report (my)", "nodes": [ ... ], "connections": { ... }, "settings": { "executionOrder": "v1" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 파일 하나에 워크플로우 하나. 노드 `name`은 영어로, 겹치지 않게. **API 키를 JSON에 쓰지 않는다.**
|
||||||
|
- **숫자는 Code가 계산한다.** LLM에게 통계를 계산하게 하지 않는다 (LLM은 계산된 통계를 글로 설명만).
|
||||||
|
|
||||||
|
## 5. 검증된 노드 (n8n 2.42.6에서 실행 확인)
|
||||||
|
|
||||||
|
### 로그 가져오기 (HTTP POST) → 계산 (Code)
|
||||||
|
```json
|
||||||
|
{"name": "Get logs", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [220, 300],
|
||||||
|
"parameters": {"method": "POST", "url": "http://data-api:8800/logs/query", "sendBody": true, "specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}", "options": {}}}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"name": "Compute metrics", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [440, 300],
|
||||||
|
"parameters": {"jsCode": "const rows = $json.logs ?? [];\nconst pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\n...\nreturn [{ json: out }];"}}
|
||||||
|
```
|
||||||
|
- 로그 600여 줄은 Code 하나에서 계산해도 충분히 빠르다 (수업 준비 때 0.1초 안팎).
|
||||||
|
|
||||||
|
### 다른 내 웹훅을 부르기 (주간 보고)
|
||||||
|
```json
|
||||||
|
{"name": "This week", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [220, 300],
|
||||||
|
"parameters": {"method": "POST", "url": "http://localhost:5678/webhook/my-log-report", "sendBody": true, "specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from, to: $json.body.to }) }}", "options": {"timeout": 30000}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Agent + OpenAI chat model (요약 글만)
|
||||||
|
- 67~76일차와 같다. `text`에는 필요한 통계만 JSON으로(`={{ JSON.stringify({ total: $json.total, failures: $json.failures, … }) }}`), 지시문에 "통계에 있는 숫자만 · 원인을 짐작하지 말 것".
|
||||||
|
|
||||||
|
### 라이브 헬프데스크 (제공 파일)
|
||||||
|
- `Topic`(Code, 낱말로 주제 · 시작 시각) → `Search FAQ`(HTTP, **timeout 1000ms**, 실패하면 오류 출력 → `Failed`) → `FAQ done`(FAQ 단계 시간) → `Found?` → `Answer`(Agent) 또는 `Not found` → `Log …`(POST `/logs`, `steps.faq_ms` · `steps.answer_ms` 포함) → 응답.
|
||||||
|
- 실행 기록은 갈래마다 같은 모양으로 남긴다 (2절 "실행 기록 한 줄").
|
||||||
|
|
||||||
|
## 6. 연결 · 7. 표현식
|
||||||
|
|
||||||
|
- 72~76일차와 같다. 다른 노드 뒤에서 웹훅 입력은 `$('Report webhook').first().json.body`.
|
||||||
|
- 키에 점이 없는 결과 필드는 테스트에서 `by_day.2026-10-26.errors`처럼 내려간다. 키에 점이 있으면(`1.2.0`) `contains`로 JSON 글자를 찾는다.
|
||||||
|
|
||||||
|
## 8. 테스트 파일 (`cases/*.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"webhook": "my-log-report", "cases": [
|
||||||
|
{"name": "둘째 주", "request": "requests/week2.json", "expect": {"equals": {"total": 357, "errors": 21, "error_rate": 0.0588}}},
|
||||||
|
{"name": "고친 뒤", "body": {"tag": "after", "source": "live"}, "expect": {"equals": {"errors": 0}}}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
- 케이스마다 `body` 또는 `request`(파일), 다른 웹훅은 `"webhook"`, 데이터 API를 읽을 때는 `"url"`(POST). 파일 맨 위 `before`는 테스트 전에 한 번 (`/logs-reset` · 고장 스위치 등). `"range": {"steps.faq_ms.p50": [1150, 3500]}`는 범위 (오늘 실습 기록의 시간처럼 실행마다 다른 값).
|
||||||
|
- `--webhook base-report`를 붙이면 같은 케이스를 다른 웹훅에 돌린다 (기준과 비교).
|
||||||
|
- 기대 숫자는 두 주 기록(`data-api/data/run-logs.json`)을 3절 정의대로 계산한 값입니다. **테스트를 통과시키려고 기대 결과나 로그 파일을 바꾸지 않는다.** 정의가 다르다고 생각되면 사람에게 묻는다.
|
||||||
|
|
||||||
|
## 9. 하지 말 것
|
||||||
|
|
||||||
|
- `tools/`, `data-api/`(로그 파일 포함), `requests/`, `reference/`, 이 파일을 고치지 않는다 (요청받은 경우만).
|
||||||
|
- 워크플로우 안에서 `/logs-reset` · `/faq/add` · `/faq-reset` · `/alerts-clear` · 고장 스위치를 부르지 않는다.
|
||||||
|
- `reference/baseline-report.json`을 고쳐 테스트를 통과시키지 않는다 (오늘은 기준과 **비교**하는 연습).
|
||||||
|
- 요약 글에 통계에 없는 숫자 · 짐작한 원인을 넣지 않는다.
|
||||||
|
- n8n 컨테이너를 지우거나 `docker compose down`을 하지 않는다. `~/day65-n8n`을 읽거나 고치지 않는다. Credential을 새로 만들지 않는다. `.env`나 API 키를 출력하지 않는다.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@AGENTS.md
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Lab 01 · 실행 기록을 읽고, 지표로 만들기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-01-Q-READ-AND-MEASURE.md](LAB-01-Q-READ-AND-MEASURE.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 실습을 5개로 늘린 뒤 정답 워크플로우를 학생 순서대로 2026-10-31에 1회씩 실행한 값입니다 (강사 Mac). 지표는 두 주 기록을 정의대로 센 것이라 누가 돌려도 같습니다. 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 결과 (에이전트 없이, `answers/01`)
|
||||||
|
|
||||||
|
| 질문 | `logs` (그대로) | 기준 보고서 | 다른 까닭 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 두 주 실행 | 666 | **602** | 기준은 `started_at < to` → 끝날(10/30, 64건)이 빠짐 |
|
||||||
|
| 오류율 | 21건 · 3.2% | **3.488…** | 백분율 · 반올림 없음 · 분모 602 |
|
||||||
|
| 10/26 하루 | 70 · 오류 16 | **0 · `null`** | 끝날을 빼니 하루가 통째로 빠짐, 0 / 0 |
|
||||||
|
| 느린 정도 | (p95는 예제 2) | 평균 1249.2ms | p50 · p95 없음 |
|
||||||
|
| 오류 갈래 | 오류 21 | 없음 | `route: null`을 건너뜀 (갈래 합 581 ≠ 602) |
|
||||||
|
|
||||||
|
- 풀이 때: "기준이 틀렸나?" → 아니오, **정의가 다르다.** 운영 회의에서 "두 주에 602건"이라고 말해 온 것이 문제입니다. 차이 64를 보고 끝날을 의심하는 것이 첫 실마리입니다 (▶ 1단계).
|
||||||
|
- 하루에 오류 16건(22.9%)이 몰린 것은 Lab 02의 이상 시간대로 이어집니다.
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 을 새로 만들어 줘. 웹훅 POST my-log-report, 이름 "77 Log report (my)".
|
||||||
|
Get logs(HTTP POST http://data-api:8800/logs/query) 에 웹훅 본문의 from · to · tag · source 를 넘기고, source 가 없으면 all.
|
||||||
|
Compute metrics(Code) 하나에서 AGENTS.md 3절 정의대로 total · errors · error_rate · p50_ms · p95_ms · by_route 를 계산해 줘.
|
||||||
|
error_rate 는 errors / total 을 소수 4자리 반올림, 0건이면 0.
|
||||||
|
p50 · p95 는 duration_ms 를 오름차순으로 놓고 ceil(p × n)번째 값 (1부터 셈, 오류 실행도 포함). route 가 null 이면 by_route 의 "오류".
|
||||||
|
LLM 은 쓰지 않는다. 기대 결과 cases/lab01.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `my-log-report` | AGENTS.md 3절 계약 · 테스트 파일의 `webhook` |
|
||||||
|
| ② | `all` | 두 주 기록(`seed`)과 오늘 실습 기록(`live`)을 모두. 기본값이 없으면 `source`를 안 보낸 요청이 0건 |
|
||||||
|
| ③ | `4` | 3절 정의. 0.031531…을 0.0315로 (기준 보고서는 3.488…%를 그대로 냄) |
|
||||||
|
| ④ | `ceil(p × n)` | 가장 가까운 순위(nearest-rank). 666건이면 p95는 633번째. 평균이나 보간이 아님 |
|
||||||
|
|
||||||
|
정답: [reference/lab01-example2-report.json](reference/lab01-example2-report.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/02`, 기준에 같은 테스트) | **0/5** · `total` 602 · 245 · 293 · 0, 오류율 백분율, `p50`/`p95` 없음, `오류` 갈래 없음 |
|
||||||
|
| 고친 뒤 (`answers/03`) | **5/5** · 두 주 666 · 21 · 0.0315 · p50 1205 · p95 2537, 둘째 주 357 · 0.0588 · p95 2598, 0.1초 안팎 |
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/04`)
|
||||||
|
|
||||||
|
| 경계 | 내 보고서 | 기준 | 기준이 틀린 까닭 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 하루 (from = to) | ✔ 70 · 16 | ✘ 0 | 끝날 제외 |
|
||||||
|
| 빈 기간 | ✔ 0 · 0 · 0 | ✘ `null` | 0 / 0, p95 칸 없음 |
|
||||||
|
| 주말 | ✔ 0 | ✔ 0 | 우연히 맞음 (실행 없는 날) |
|
||||||
|
| 기간 없음 | ✔ 666 | ✔ 666 | 우연히 맞음 (거를 것 없음) |
|
||||||
|
| `source: seed` | ✔ 666 | ✘ 602 | 끝날 제외 |
|
||||||
|
| `source: live` | ✔ 0 | ✘ **666** | 본문의 `source`를 무시 (`{source: 'all'}` 고정) |
|
||||||
|
|
||||||
|
- 내 보고서 **6/6**, 기준 **2/6**. 이 예제의 답: 기준이 맞은 둘은 정의가 맞아서가 아니라 **경계가 문제를 가려서**. `source: live` 666은 Lab 03에서 "오늘 기록만" 볼 때 위험합니다.
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 by_day 와 by_release 를 더해 줘.
|
||||||
|
by_day: started_at 앞 10자(날짜)로 묶어 {"2026-10-26": {total, errors, error_rate}}, 날짜순.
|
||||||
|
by_release: route 가 "자동 답변" 인 실행만 release 로 묶어 {answers, repeats, repeat_rate}.
|
||||||
|
repeats 는 repeat 가 true 인 수, repeat_rate = repeats / answers (소수 4자리).
|
||||||
|
앞의 지표는 그대로 둔다. 기대 결과 cases/lab01-challenge.json, cases/lab01.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `10` | `2026-10-26T09:05:39` → `2026-10-26` |
|
||||||
|
| ② | `자동 답변` | 답을 준 실행만. 담당자 연결 · 오류는 '다시 물을' 답이 없음 |
|
||||||
|
| ③ | `true` | 같은 사람이 10분 안에 같은 주제를 다시 물음 (AGENTS.md 2절) |
|
||||||
|
| ④ | 그대로 | 같은 파일에 더함. `lab01` 5/5가 그대로여야 함 |
|
||||||
|
|
||||||
|
정답: [answers/77-lab01-challenge-day-release.json](answers/77-lab01-challenge-day-release.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/05`) | **0/6** · 칸 없음 |
|
||||||
|
| 고친 뒤 (`answers/06`) | **6/6** · 10/26 70 · 16 · 0.2286, 10/29 97 · 5, 1.0.0 402 · 19 · 0.0473, 1.1.0 181 · 14 · 0.0773, 1.2.0 **8** · 5 · 0.625. `lab01` 5/5 |
|
||||||
|
|
||||||
|
- 팀장에게 답: 오류는 **월요일(16) · 목요일(5)** 이틀뿐. 1.1.0의 다시 묻는 비율이 1.0.0보다 높음(7.73% > 4.73%). 1.2.0의 62.5%는 8건이라 판단 보류 — 비율 옆에 건수.
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 steps 와 비용을 더해 줘.
|
||||||
|
steps: faq_ms · answer_ms 마다, steps 의 그 값이 0보다 큰 실행만 모아 {runs, p50, p95} (백분위 정의는 앞과 같음, 없으면 runs 0 · p50 0 · p95 0).
|
||||||
|
cost_usd: 실행마다 cost_usd 의 합, 소수 5자리 반올림. cost_per_run: cost_usd / total, 소수 6자리 (0건이면 0).
|
||||||
|
llm_calls: 실행마다 llm_calls 의 합.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab01-challenge2.json, lab01 · lab01-challenge 도 그대로 통과.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | 0보다 큰 | 그 단계를 거치지 않은 실행(`잠시 후` 20건은 FAQ도 답도 0)을 빼야 단계의 진짜 시간 |
|
||||||
|
| ② | `5` | 0.07836처럼 한 건 0.00012달러 단위가 보이게 |
|
||||||
|
| ③ | 합 | 실행마다 LLM을 부른 횟수(0 또는 1)를 더함 → 596 |
|
||||||
|
| ④ | `lab01 · lab01-challenge` | 앞에서 통과한 테스트가 그대로인지 (회귀) |
|
||||||
|
|
||||||
|
정답: [answers/77-lab01-challenge2-steps-cost.json](answers/77-lab01-challenge2-steps-cost.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/07`) | **0/5** · 칸 없음 |
|
||||||
|
| 고친 뒤 (`answers/08`) | **5/5** · 두 주 FAQ p95 39ms (646건) · 답 p50 1179 · p95 2509ms (596건), 월요일 FAQ p95 **1000ms**, 비용 0.07836 · 실행당 0.000118 · 호출 596. `lab01` 5/5 · `lab01-challenge` 6/6 |
|
||||||
|
|
||||||
|
- 운영팀에게 답: 평소 느린 것은 **LLM 답 단계**, 월요일에는 **FAQ 단계**(timeout 1초). 0인 단계 값을 넣으면 답 단계 p50이 작아짐 — `runs`(646 · 596)를 함께 보는 까닭.
|
||||||
@@ -0,0 +1,751 @@
|
|||||||
|
# Lab 01 · 실행 기록을 읽고, 지표로 만들기
|
||||||
|
|
||||||
|
- 관련 강의: [1단계 · 실행 기록 읽기](../../Lecture/77/01-STAGE1-READ-THE-LOG.md)
|
||||||
|
- 소요 시간: 50분 (2교시 한 시간) · 예제 1 10분 · 예제 2 15분 · 예제 3 7분 · 과제 1 8분 · 과제 2 10분
|
||||||
|
- 준비: [실습 준비](../../Lecture/77/00-DAY77-PREP.md)의 `doctor` → `ready`, 기준 보고서 `base-report` 게시
|
||||||
|
- 목표: 두 주 동안의 실행 기록을 먼저 **눈으로** 읽고, 팀이 쓰던 기준 보고서의 숫자가 왜 다른지 찾는다. 같은 질문에 늘 같은 숫자로 답하는 **지표 보고서**를 만들고(지표는 정의가 먼저), 경계(하루 · 빈 기간 · 거르기)에서 재 보고, 날짜별 · 릴리스별 · 단계별 · 비용으로 나눠 본다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
E1["예제 1<br/>손으로 읽기 ·<br/>기준 보고서"] --> E2["예제 2<br/>지표 보고서"] --> E3["예제 3<br/>경계에서 재 보기"] --> T1["과제 1<br/>날짜별 · 릴리스별"] --> T2["과제 2<br/>단계별 · 비용"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 복사해 붙일 요청문은 없습니다. 막히면 `▶` 를 하나씩 펼칩니다. 연습이 끝나면 `uv run tools/flow.py ops --reset`.
|
||||||
|
|
||||||
|
## 시작하기 · 터미널 두 개
|
||||||
|
|
||||||
|
| 터미널 | 하는 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| ① 에이전트 | `cd ~/aio-01d4-stu/Last3Mon/77` 후 `codex` (또는 `claude`, `agy`). 내가 쓴 요청문을 보낸다 |
|
||||||
|
| ② 확인 | `cd ~/aio-01d4-stu/Last3Mon/77` 에서 `logs` · `call` · `push` · `test`를 **내가** 실행한다 |
|
||||||
|
|
||||||
|
`cp templates/OPS-ANALYSIS-TEMPLATE.md my-work/OPS.md` 후 표를 채워 갑니다. 쓴 요청문은 `my-work/PROMPTS.md`에 모읍니다.
|
||||||
|
|
||||||
|
## 예제 1 · 손으로 읽기 · 팀의 기준 보고서 (10분, 에이전트 없이)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 두 주 기록을 직접 훑어보고, 팀이 지난달 급히 만든 기준 보고서(`base-report`)에 같은 기간을 물어 **두 숫자가 다른지** 봅니다. 고치는 것은 예제 2부터이고, 여기서는 **읽고 비교해 적기만** 합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 가게의 영수증 더미와 장부.** 가게 계산대 옆에 두 주 치 영수증이 쌓여 있습니다. 사장님이 "두 주 동안 몇 건 팔았지?"라고 물으면, 알바생 둘이 서로 다른 답을 할 수 있습니다. 한 사람은 마지막 날 영수증을 빼고 셌고, 한 사람은 넣고 셌다면요. 예제 1은 영수증(실행 기록)을 직접 넘겨 보고, 팀 장부(기준 보고서)가 어떻게 셌는지 비교하는 실습입니다.
|
||||||
|
|
||||||
|
**상황.** 데이터 API에 IT 헬프데스크의 두 주(10/19~10/30, 평일 10일) 실행 기록 666줄이 있습니다. 한 줄이 헬프데스크가 질문 하나에 답한 한 번의 실행입니다. 팀에는 지난달 만든 보고서 워크플로우 `base-report`가 있고, 운영 회의에서 이 숫자를 써 왔습니다. 그런데 이 보고서가 맞는지 아무도 확인하지 않았습니다.
|
||||||
|
|
||||||
|
**실험 넷.**
|
||||||
|
|
||||||
|
| 실험 | 명령 | 볼 것 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① 두 주를 훑기 | `logs --from 2026-10-19 --to 2026-10-30` | 실행 수 · 오류 수 · 갈래(`route`)마다 수, 마지막 다섯 줄 |
|
||||||
|
| ② 하루만 | `logs --from 2026-10-26 --to 2026-10-26` | 월요일 하루의 실행 · 오류 — 두 주의 오류와 비교 |
|
||||||
|
| ③ 기준에 두 주 | `call base-report requests/two-weeks.json` | ①과 같은 숫자인가? 어떤 칸이 있고 없나? |
|
||||||
|
| ④ 기준에 하루 | `call base-report requests/monday.json` | ②와 같은가? |
|
||||||
|
|
||||||
|
`logs`는 데이터 API의 기록을 **그대로** 세어 보여 주는 도구이고, `base-report`는 팀이 만든 워크플로우가 **자기 방식으로** 센 결과입니다. 둘이 다르면 어느 한쪽의 "세는 방법"이 다른 것입니다.
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 같은 기록이라도 **무엇을 넣고 빼는지 · 무엇으로 나누는지 · 몇 자리로 끊는지**에 따라 숫자가 달라집니다. 그것을 정해 둔 것이 지표의 **정의**이고, 예제 2에서 정의대로 다시 셉니다.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 1.1 표 (질문 · 내 짐작 · 기준 보고서 · 다른 까닭).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ① 두 주를 훑기
|
||||||
|
uv run tools/flow.py logs --from 2026-10-19 --to 2026-10-30 --last 5
|
||||||
|
|
||||||
|
# ② 하루만 (숫자만, --last 0)
|
||||||
|
uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 0
|
||||||
|
|
||||||
|
# ③ · ④ 팀의 기준 보고서에 같은 기간을
|
||||||
|
uv run tools/flow.py call base-report requests/two-weeks.json # {"from": "2026-10-19", "to": "2026-10-30"}
|
||||||
|
uv run tools/flow.py call base-report requests/monday.json # {"from": "2026-10-26", "to": "2026-10-26"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**실행 기록 한 줄에 무엇이 있나.** [AGENTS.md](AGENTS.md) 2절 "실행 기록 한 줄"입니다. 오늘 질문에 쓰는 칸은 이것뿐입니다.
|
||||||
|
|
||||||
|
| 칸 | 뜻 | 오늘 쓰는 곳 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `started_at` | 실행이 시작된 때 (`2026-10-26T09:05:39`) | 기간 · 날짜별 · 시간대별 |
|
||||||
|
| `status` | `success` · `error` | 오류 수 · 오류율 |
|
||||||
|
| `route` | `자동 답변` · `담당자 연결` · `잠시 후`, 오류면 `null` | 갈래별 · 답 못 함 |
|
||||||
|
| `duration_ms` | 한 실행에 걸린 시간 | p50 · p95 |
|
||||||
|
| `error_node` · `error_message` | 오류가 난 노드 · 그 말 | 실패 묶음 (Lab 02) |
|
||||||
|
| `topic` · `release` · `repeat` | 질문 주제 · 버전 · 10분 안에 다시 물음 | 릴리스별 · 다시 묻는 주제 |
|
||||||
|
| `steps` · `cost_usd` · `llm_calls` | 단계마다 시간 · 비용 · LLM 호출 수 | 과제 2 |
|
||||||
|
|
||||||
|
**지표는 "정의 + 계산"입니다.** "오류율"이라는 말만으로는 숫자가 하나로 정해지지 않습니다. 기간의 끝날을 넣나? 0.0315로 쓰나 3.15로 쓰나? 몇 자리에서 끊나? 느린 정도는 평균인가 p95인가? 이것을 미리 정해 둔 것이 정의이고, 정의가 같아야 지난주와 이번 주, 내 숫자와 옆 사람 숫자를 비교할 수 있습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
L[("실행 기록 666줄")] --> A["logs<br/>그대로 셈"]
|
||||||
|
L --> B["base-report<br/>팀의 방식으로 셈"]
|
||||||
|
A --> C{"같은 숫자?"}
|
||||||
|
B --> C
|
||||||
|
C -->|다르면| D["세는 방법(정의)이<br/>어디서 다른가"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**기대 결과.** `logs`의 두 주 · 하루 숫자와 기준 보고서의 숫자가 **다릅니다.** 어느 칸이 왜 다른지 `OPS.md` 1.1에 적습니다. 무엇이 다른지는 아래 실행 결과를 보기 전에 먼저 찾아 봅니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 두 숫자가 다르면 무엇부터 보나</summary>
|
||||||
|
|
||||||
|
차이가 **얼마**인지 봅니다. 두 주 666과 기준의 숫자의 차이가 어느 하루의 실행 수와 같다면? `logs --from 2026-10-30 --to 2026-10-30 --last 0`으로 확인해 봅니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 2단계 · 기준의 오류율은 무엇으로 나눴나</summary>
|
||||||
|
|
||||||
|
`logs`는 오류율을 `3.2%`로 보여 줍니다. 기준의 `error_rate`는 어떤 모양인가요? 백분율인가 비율인가, 몇 자리인가? 하루만 물었을 때의 `null`은 무엇을 0으로 나눈 것일까요?
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 실행 결과 · 손으로 읽기 · 기준 보고서
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다. 두 주 기록은 바뀌지 않으므로 숫자는 누가 돌려도 같습니다 (`call`의 시간만 다름).
|
||||||
|
|
||||||
|
이 예제는 **고치지 않습니다.** 같은 기간을 `logs`(그대로 셈)와 기준 보고서(팀의 방식)에 물어 비교합니다. 고치는 것은 예제 2입니다.
|
||||||
|
|
||||||
|
#### ① · ② `logs`로 두 주 · 하루
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py logs --from 2026-10-19 --to 2026-10-30 --last 5
|
||||||
|
uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 0
|
||||||
|
```
|
||||||
|
|
||||||
|
출력:
|
||||||
|
|
||||||
|
```text
|
||||||
|
666 run(s) · errors 21 (3.2%)
|
||||||
|
route: 자동 답변 591 · 담당자 연결 34 · 오류 21 · 잠시 후 20
|
||||||
|
2026-10-30T17:05:38 success 자동 답변 1311ms 프린터 인쇄 대기열이 안 비워져요
|
||||||
|
2026-10-30T17:11:46 success 자동 답변 1175ms 메일 메일함 용량이 꽉 찼대요
|
||||||
|
2026-10-30T17:20:51 success 담당자 연결 79ms 와이파이 회의실 와이파이가 안 잡혀요
|
||||||
|
2026-10-30T17:24:39 success 자동 답변 1205ms 메일 첨부 파일이 너무 커서 안 보내져요
|
||||||
|
2026-10-30T17:59:20 success 자동 답변 1076ms 메일 메일함 용량이 꽉 찼대요
|
||||||
|
|
||||||
|
70 run(s) · errors 16 (22.9%)
|
||||||
|
route: 자동 답변 50 · 오류 16 · 담당자 연결 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나**
|
||||||
|
|
||||||
|
- 두 주 666건 중 오류 21건(3.2%). 그런데 **하루(10/26 월요일)에 16건**이 몰려 있습니다. 그날의 오류율은 22.9%로, 두 주 평균의 7배쯤입니다. "두 주 동안 오류율 3%"라는 말만으로는 이 사실이 보이지 않습니다.
|
||||||
|
- 마지막 다섯 줄은 10/30(금) 저녁의 실행입니다. 담당자 연결 한 줄(와이파이, 79ms)이 눈에 띕니다. 답을 찾지 못해 바로 사람에게 넘긴 실행입니다 (Lab 02).
|
||||||
|
|
||||||
|
#### ③ · ④ 기준 보고서에 같은 기간
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py call base-report requests/two-weeks.json
|
||||||
|
uv run tools/flow.py call base-report requests/monday.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP 200 · 1.2s
|
||||||
|
{ "total": 602, "errors": 21, "error_rate": 3.488372093023256, "avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": { "자동 답변": 529, "담당자 연결": 32, "잠시 후": 20 } }
|
||||||
|
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{ "total": 0, "errors": 0, "error_rate": null, "avg_ms": null, "by_route": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나**
|
||||||
|
|
||||||
|
| 칸 | `logs` | 기준 보고서 | 다른 까닭 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 두 주 실행 | 666 | **602** | 666 − 602 = 64 = 10/30 하루의 실행. 기준은 `started_at < to`로 걸러 **끝날을 뺐습니다** (`2026-10-30T09:…`는 `2026-10-30`보다 큰 글자) |
|
||||||
|
| 오류율 | 3.2% (0.0315) | **3.488…** | 백분율(×100)이고, 끊지 않았고, 분모가 602 |
|
||||||
|
| 느린 정도 | (p95는 예제 2) | 평균 1249.2ms | 평균만 있음. 대부분이 겪는 시간을 말하지 못함 |
|
||||||
|
| 오류로 끝난 실행 | 갈래 `오류` 21 | 갈래에 **없음** | `route`가 `null`인 줄을 건너뜀. 갈래를 더하면 581 ≠ 602 |
|
||||||
|
| 하루 (from = to) | 70 · 오류 16 | **0 · `null`** | 끝날을 빼니 하루를 물으면 아무것도 남지 않고, 0을 0으로 나눠 `null` |
|
||||||
|
|
||||||
|
- 기준 보고서는 "고장"이 아닙니다. 200을 주고, 숫자도 그럴듯합니다. **세는 방법이 다를 뿐**인데, 운영 회의에서는 이 숫자로 "두 주에 602건"이라고 말해 왔습니다.
|
||||||
|
- 이 차이를 `OPS.md` 1.1에 적습니다. 예제 2에서 정의를 정하고, 예제 3에서 경계를 더 재 봅니다.
|
||||||
|
|
||||||
|
## 예제 2 · 지표 보고서 (15분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 예제 1에서 본 차이를 없애기 위해, AGENTS.md 3절 정의대로 세는 **내 보고서**(`my-log-report`)를 만듭니다. Lab 01의 중심 실습이고, 오늘 만드는 모든 것이 이 보고서를 바탕으로 합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 가게의 장부 쓰는 법.** 알바생마다 세는 법이 다르면 장부를 믿을 수 없습니다. 그래서 가게는 "마감일 영수증도 넣는다, 금액은 원 단위까지, 환불도 따로 적는다"처럼 **장부 쓰는 법**을 벽에 붙여 둡니다. 예제 2는 그 쓰는 법(정의)을 요청문에 그대로 넣어, 누가 만들어도 같은 숫자가 나오는 보고서를 만드는 실습입니다.
|
||||||
|
|
||||||
|
| 예제 1에서 본 것 | 정의 (AGENTS.md 3절) | 확인하는 케이스 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 마지막 날이 빠짐 | 날짜는 **포함** (`from ≤ 날짜 ≤ to`, 데이터 API가 거름) | 두 주 · 첫 주 · 둘째 주 · 하루 |
|
||||||
|
| 오류율이 백분율 · 끊지 않음 | `errors / total`, 소수 4자리, 0건이면 0 | 두 주 · 둘째 주 |
|
||||||
|
| 평균만 있음 | `p50_ms` · `p95_ms` = 오름차순 `ceil(p × n)`번째 | 두 주 · 둘째 주 |
|
||||||
|
| 오류가 갈래에 없음 | `route`가 `null`이면 `"오류"` | 두 주 · 갈래별 |
|
||||||
|
|
||||||
|
**순서.** 같은 테스트를 **기준에 먼저** 돌려 0/5를 봄 (케이스마다 무엇이 틀렸는지) → 요청문을 써서 에이전트가 새 파일을 만들게 함 → push → 5/5.
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 정의를 요청문에 적으면 에이전트도 같은 숫자를 낸다는 것, p95가 평균과 어떻게 다른지, 숫자는 LLM이 아니라 Code가 세야 하는 까닭.
|
||||||
|
|
||||||
|
**만들 것**: 기간(`from` · `to`)을 받아 실행 기록을 가져와 지표를 계산해 돌려주는 워크플로우.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01.json --webhook base-report -v # 고치기 전 (기준에 같은 테스트)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 파일 · 웹훅 | `my-work/log-report.json` (새로) · `my-log-report` |
|
||||||
|
| 지표 | `total`, `errors`, `error_rate`, `p50_ms`, `p95_ms`, `by_route` — **정의는 [AGENTS.md](AGENTS.md) 3절** |
|
||||||
|
| 계산 | Code 노드 하나 (LLM 없음) |
|
||||||
|
| 기대 결과 | `cases/lab01.json` (두 주 · 갈래별 · 첫 주 · 둘째 주 · 하루) |
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**지금 무엇이 문제인가.** 기준 보고서는 틀린 계산을 한 것이 아니라 **다른 정의**로 셌습니다. 그래서 고치는 일은 "버그 찾기"보다 "정의를 정해 그대로 세기"입니다. 정의가 요청문에 없으면 에이전트도 자기 방식(평균, 보간한 p95, 백분율…)으로 셉니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 기준
|
||||||
|
A1(["from · to"]) --> B1["로그 전체를 가져와<br/>started_at < to 로 거름"] --> C1["평균 · 백분율<br/>오류 갈래 없음"]
|
||||||
|
end
|
||||||
|
subgraph 내 보고서
|
||||||
|
A2(["from · to · tag · source"]) --> B2["데이터 API가 거름<br/>(날짜 포함)"] --> C2["Code: 정의대로<br/>p50 · p95 · 소수 4자리 · 오류 갈래"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**p95란.** 실행 시간을 짧은 것부터 줄 세웠을 때 95%째에 있는 값입니다. "100명 중 95명은 이 시간 안에 답을 받았다"는 뜻입니다. 오늘 정의는 **가장 가까운 순위**(nearest-rank)입니다: n개를 오름차순으로 놓고 `ceil(0.95 × n)`번째. 예를 들어 값이 10개면 `ceil(9.5)` = 10번째, 666개면 `ceil(632.7)` = 633번째입니다. 평균은 아주 느린 몇 건에 끌려가고, 대부분의 사용자가 겪는 시간을 말하지 못합니다.
|
||||||
|
|
||||||
|
| 같은 기록 | 평균 | p50 | p95 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1초 × 9건 + 10초 × 1건 | 1.9초 | 1초 | 10초 |
|
||||||
|
|
||||||
|
**왜 Code인가.** LLM에게 666줄을 주고 "오류율이 얼마야?"라고 물으면 틀릴 수 있고(세는 일은 LLM이 약함), 줄이 많을수록 돈이 듭니다 (75일차). 숫자는 Code가 정확하게, 빠르게(0.1초 안팎) 셉니다. LLM은 Lab 02 과제 2에서 **센 결과를 글로 설명**할 때만 씁니다.
|
||||||
|
|
||||||
|
**테스트 케이스.**
|
||||||
|
|
||||||
|
| 케이스 | 보내는 것 | 기대 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 두 주 전체 | `two-weeks` | `total` 666 · `errors` 21 · `error_rate` 0.0315 · `p50_ms` 1205 · `p95_ms` 2537 |
|
||||||
|
| 두 주 · 갈래별 | `two-weeks` | 자동 답변 591 · 담당자 연결 34 · 잠시 후 20 · 오류 21 |
|
||||||
|
| 첫 주 | `week1` | 309 · 0 · 0 |
|
||||||
|
| 둘째 주 | `week2` | 357 · 21 · 0.0588 · p95 2598 |
|
||||||
|
| 10월 26일 하루 | `monday` | 70 · 16 |
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전(기준에 같은 테스트) 0/5, 고친 뒤 5/5.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 정의를 요청문에</summary>
|
||||||
|
|
||||||
|
"p95를 구해 줘"만 쓰면 에이전트마다 계산이 다릅니다 (평균? 보간?). 3절의 정의를 그대로 요청문에 넣거나 "AGENTS.md 3절 정의대로"라고 씁니다. 반올림 자리, 0건일 때도.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 2단계 · 거르기는 누가</summary>
|
||||||
|
|
||||||
|
기간 · `tag` · `source`로 거르는 일은 데이터 API(`POST /logs/query`)가 합니다 ([AGENTS.md](AGENTS.md) 2절). 워크플로우는 웹훅 본문을 그대로 넘기면 됩니다. 기준처럼 다 가져와서 직접 거르면 정의가 두 곳에 생깁니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 파일 · 웹훅 ② 넘길 본문과 기본값 ③ 지표와 정의(반올림 · 0건) ④ 백분위 정의 ⑤ LLM 없음 · 기대 결과**. 아래 빈칸 4개(`____①`~`____④`)를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 을 새로 만들어 줘. 웹훅 POST ____①, 이름 "77 Log report (my)".
|
||||||
|
Get logs(HTTP POST http://data-api:8800/logs/query) 에 웹훅 본문의 from · to · tag · source 를 넘기고, source 가 없으면 ____②.
|
||||||
|
Compute metrics(Code) 하나에서 AGENTS.md 3절 정의대로 total · errors · error_rate · p50_ms · p95_ms · by_route 를 계산해 줘.
|
||||||
|
error_rate 는 errors / total 을 소수 ____③자리 반올림, 0건이면 0.
|
||||||
|
p50 · p95 는 duration_ms 를 오름차순으로 놓고 ____④번째 값 (1부터 셈, 오류 실행도 포함). route 가 null 이면 by_route 의 "오류".
|
||||||
|
LLM 은 쓰지 않는다. 기대 결과 cases/lab01.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트 (답이 아니라 **어디서 찾고 무엇을 생각하나**):
|
||||||
|
|
||||||
|
- ① AGENTS.md 3절 계약 표의 "로그 보고서" 줄입니다. 테스트 파일 `cases/lab01.json` 맨 위의 `webhook`과 같아야 합니다.
|
||||||
|
- ② `source`의 세 값(`all` · `seed` · `live`) 중, 요청에 `source`가 없을 때 두 주 기록과 오늘 기록을 **모두** 보는 값.
|
||||||
|
- ③ 3절 지표 표의 `error_rate` 줄. 테스트의 기대값 0.0315를 보면 몇 자리인지 보입니다.
|
||||||
|
- ④ 3절 지표 표의 `p50_ms` · `p95_ms` 줄의 굵은 글씨입니다. 위 "p95란"의 예와 같이.
|
||||||
|
|
||||||
|
채운 요청문은 수업 뒤 공개되는 정답본의 요청문 예와 비교합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01.json -v
|
||||||
|
uv run tools/flow.py call my-log-report requests/week2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 지표 보고서
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 지표 숫자는 두 주 기록을 정의대로 센 것이라 누구에게나 같고, 시간만 조금씩 다릅니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 기준(`base-report`)에 같은 테스트
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01.json --webhook base-report -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만, `-v`의 요청 · 응답은 줄임):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 두 주 전체 · 0.2s · total = 602 (expected 666); error_rate = 3.488372093023256 (expected 0.0315); p50_ms = null (expected 1205); p95_ms = null (expected 2537)
|
||||||
|
✘ 두 주 · 갈래별 · 0.1s · by_route.자동 답변 = 529 (expected 591); by_route.담당자 연결 = 32 (expected 34); by_route.오류 = null (expected 21)
|
||||||
|
✘ 첫 주 · 0.1s · total = 245 (expected 309)
|
||||||
|
✘ 둘째 주 · 0.1s · total = 293 (expected 357); error_rate = 7.167235494880546 (expected 0.0588); p95_ms = null (expected 2598)
|
||||||
|
✘ 10월 26일 하루 · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
|
||||||
|
Passed 0/5
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- **`total`이 모두 작음:** 602 · 245 · 293 · 0. 기간마다 **끝날의 실행 수만큼** 빠졌습니다 (첫 주 309 − 245 = 64 = 10/23, 둘째 주 357 − 293 = 64 = 10/30). 하루를 물으면 끝날이 곧 그날이라 0입니다.
|
||||||
|
- **`error_rate`가 3.488… · 7.167…:** 백분율에, 반올림이 없고, 분모도 줄어든 `total`입니다. 기대값 0.0315는 "비율, 소수 4자리"입니다.
|
||||||
|
- **`p50_ms` · `p95_ms`가 `null`:** 기준에는 그런 칸이 없습니다 (`avg_ms`만). `null`은 "그 칸이 없음"입니다.
|
||||||
|
- **`by_route.오류`가 `null`:** 오류로 끝난 실행을 갈래에서 건너뛰었습니다. 자동 답변 529 · 담당자 연결 32도 끝날이 빠진 만큼 작습니다.
|
||||||
|
- 0/5는 **예상한 결과**입니다. 다섯 케이스의 ✘가 모두 예제 1에서 찾은 정의의 차이 넷으로 설명됩니다.
|
||||||
|
|
||||||
|
#### 고친 뒤 · 내 `my-log-report`
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01.json -v
|
||||||
|
uv run tools/flow.py call my-log-report requests/week2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과 `call`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 두 주 전체 · 0.9s
|
||||||
|
✔ 두 주 · 갈래별 · 0.1s
|
||||||
|
✔ 첫 주 · 0.2s
|
||||||
|
✔ 둘째 주 · 0.1s
|
||||||
|
✔ 10월 26일 하루 · 0.0s
|
||||||
|
Passed 5/5
|
||||||
|
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{ "total": 357, "errors": 21, "error_rate": 0.0588, "p50_ms": 1171, "p95_ms": 2598,
|
||||||
|
"by_route": { "오류": 21, "자동 답변": 299, "담당자 연결": 17, "잠시 후": 20 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 케이스 | 고치기 전 (기준) | 고친 뒤 | 무엇이 바꿨나 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 두 주 실행 | 602 | 666 | 거르기를 데이터 API에 맡김 (날짜 **포함**) |
|
||||||
|
| 두 주 오류율 | 3.488… | 0.0315 | `errors / total`, 소수 4자리 |
|
||||||
|
| 두 주 p50 · p95 | 없음 | 1205 · 2537ms | 오름차순 `ceil(p × n)`번째 |
|
||||||
|
| 오류 갈래 | 없음 | 21 | `null` → `"오류"` |
|
||||||
|
| 하루 | 0 | 70 · 오류 16 | 끝날 포함 |
|
||||||
|
|
||||||
|
- **둘째 주 응답을 읽으면:** 357건 중 오류 21, 오류율 0.0588(5.88%). 갈래를 더하면 21 + 299 + 17 + 20 = 357로 `total`과 맞습니다. 기준에서는 맞지 않던 합입니다.
|
||||||
|
- **p50 1171 · p95 2598:** 절반은 1.2초 안에, 95%는 2.6초 안에 답을 받았습니다. 첫 주 p95(2525)와 비교하면 조금 느려졌습니다. 평균 하나로는 이런 비교가 어렵습니다.
|
||||||
|
- **0.1초 안팎:** 666줄을 Code 하나에서 셌습니다. LLM은 부르지 않았습니다.
|
||||||
|
- 이제 이 보고서가 오늘의 **기준 숫자**입니다. 과제 · Lab 02 · Lab 03은 모두 이 파일에 칸을 더해 갑니다.
|
||||||
|
|
||||||
|
## 예제 3 · 경계에서 재 보기 (7분, 에이전트 없이)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 예제 2의 내 보고서와 기준 보고서에 **경계의 질문**(하루만 · 빈 기간 · 주말 · 기간 없음 · `source`)을 같이 던져, 평소에는 안 보이던 정의의 차이를 드러냅니다. 고치는 실습이 아니라 **재 보는** 실습입니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 줄자의 눈금 끝.** 줄자로 1m를 잴 때 "끝 눈금을 넣나"는 보통 상관없어 보입니다. 하지만 1cm짜리를 재면 그 한 칸이 전부입니다. 장부도 그렇습니다. 두 주를 셀 때는 그럴듯하던 장부가 "오늘 하루" · "손님이 없던 날"을 물으면 0이나 오류를 냅니다. 예제 3은 끝 눈금처럼 작은 경계에서 두 장부를 재 보는 실습입니다.
|
||||||
|
|
||||||
|
**상황.** 보통 기간(두 주 · 한 주)에서는 기준 보고서도 그럴듯한 숫자를 냅니다. 그런데 운영에서는 "오늘 하루만", "이번 주 아직 0건", "오늘 실습 기록만"처럼 경계의 질문을 자주 합니다. 같은 테스트 `cases/lab01-edges.json`을 두 보고서에 돌립니다.
|
||||||
|
|
||||||
|
| 경계 | 보내는 것 | 기대 (정의대로) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 하루 · from = to | `monday` | 70건 · 오류 16 (끝날 포함) |
|
||||||
|
| 빈 기간 | 11/02~11/06 (기록 없음) | 200 · `total` 0 · `error_rate` 0 · `p95_ms` 0 |
|
||||||
|
| 주말 | 10/24~10/25 | 0건 |
|
||||||
|
| 기간을 비우면 | `{}` | 666 (전체) |
|
||||||
|
| `source: seed` | 두 주 · 두 주 기록만 | 666 |
|
||||||
|
| `source: live` | 오늘 실습 기록만 (아직 없음) | 0 · 오류율 0 |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 정의는 경계에서 시험받는다는 것, 0으로 나누기 · 끝날 포함 · 거르기를 **처음부터** 정해 둬야 한다는 것. 그리고 테스트에 경계 케이스를 넣는 까닭.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 1.2 표 (경계 · 내 보고서 · 기준 보고서 · 기준이 틀린 까닭).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-edges.json -v # 내 보고서
|
||||||
|
uv run tools/flow.py test cases/lab01-edges.json --webhook base-report # 같은 케이스를 기준에
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**경계(edge)란.** 값의 범위 끝에 있는 경우입니다: 0건, 1일, 첫날 · 마지막 날, 거르기를 했을 때 아무것도 남지 않는 경우. 프로그램의 실수는 대부분 경계에서 나옵니다. 보통 경우만 시험하면 "잘 된다"고 믿게 됩니다.
|
||||||
|
|
||||||
|
| 경계 | 정의가 없으면 생기는 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| 끝날 | `<`와 `≤`의 차이로 하루가 통째로 빠짐 |
|
||||||
|
| 0건 | `0 / 0` → `NaN` → JSON의 `null` (화면에 빈칸, 다음 계산이 깨짐) |
|
||||||
|
| 거르기 | `source` · `tag`를 무시하면 늘 전체가 나옴 (오늘 실습 기록만 보고 싶어도) |
|
||||||
|
|
||||||
|
**`--webhook base-report`.** 같은 케이스 파일을 다른 웹훅에 돌리는 옵션입니다. 테스트 파일은 그대로 두고 두 워크플로우를 비교할 때 씁니다.
|
||||||
|
|
||||||
|
**기대 결과.** 내 보고서 6/6. 기준 보고서는 대부분 ✘ — 어느 케이스가 왜 ✘인지가 이 예제의 답입니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · `source: live`에 기준이 666을 낸다면</summary>
|
||||||
|
|
||||||
|
기준 보고서가 데이터 API에 무엇을 보내는지 봅니다: `uv run tools/flow.py diff reference/baseline-report.json my-work/log-report.json`. `Get logs` 노드의 본문을 비교해 보세요.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 실행 결과 · 경계에서 재 보기
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
이 예제는 **고치지 않습니다.** 같은 케이스 파일을 내 보고서와 기준 보고서에 돌려 비교합니다. 기준을 고치지 않는 것은, 오늘은 기준과 **비교하는** 연습이기 때문입니다.
|
||||||
|
|
||||||
|
#### 내 보고서 (`my-log-report`)
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-edges.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ 하루 · from = to (포함) · 0.0s
|
||||||
|
✔ 빈 기간 · 0건이어도 200 · 0.1s
|
||||||
|
✔ 주말 · 실행이 없는 날 · 0.0s
|
||||||
|
✔ 기간을 비우면 전체 · 0.1s
|
||||||
|
✔ source seed · 두 주 기록만 · 0.0s
|
||||||
|
✔ source live · 오늘 실습 기록 (아직 없음) · 0.0s
|
||||||
|
Passed 6/6
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나:** 첫 줄 `before`는 테스트가 먼저 오늘 실습 기록을 비운 것입니다 (`live`가 0이어야 하므로). 내 보고서는 거르기를 데이터 API에 맡기고, 0건이면 나누지 않으므로 경계에서도 정의대로입니다. 빈 기간 · 주말은 `total` 0 · `error_rate` 0 · `p95_ms` 0으로 200을 줍니다.
|
||||||
|
|
||||||
|
#### 같은 케이스를 기준에 (`base-report`)
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-edges.json --webhook base-report
|
||||||
|
```
|
||||||
|
|
||||||
|
출력:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 하루 · from = to (포함) · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
|
||||||
|
✘ 빈 기간 · 0건이어도 200 · 0.1s · error_rate = null (expected 0); p95_ms = null (expected 0)
|
||||||
|
✔ 주말 · 실행이 없는 날 · 0.1s
|
||||||
|
✔ 기간을 비우면 전체 · 0.0s
|
||||||
|
✘ source seed · 두 주 기록만 · 0.1s · total = 602 (expected 666)
|
||||||
|
✘ source live · 오늘 실습 기록 (아직 없음) · 0.1s · total = 666 (expected 0); error_rate = 3.153153153153153 (expected 0)
|
||||||
|
Passed 2/6
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (케이스마다)**
|
||||||
|
|
||||||
|
| 경계 | 기준의 결과 | 까닭 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 하루 | 0건 | `started_at < to`로 끝날을 뺌 → 하루를 물으면 그날이 통째로 빠짐 |
|
||||||
|
| 빈 기간 | 오류율 · p95 `null` | `0 / 0` = `NaN` → JSON으로 `null`. p95 칸은 원래 없음 |
|
||||||
|
| 주말 ✔ | 0건 | 실행이 없는 날이라 어떻게 세도 0 — **우연히** 맞음 |
|
||||||
|
| 기간 없음 ✔ | 666 | 거를 것이 없으면 끝날 문제도 없음 — 역시 우연 |
|
||||||
|
| `source: seed` | 602 | `source`는 맞았지만 끝날 문제가 그대로 |
|
||||||
|
| `source: live` | **666** · 3.15… | 기준은 `Get logs`에 늘 `{source: 'all'}`만 보내고 본문의 `source`를 **무시**. 오늘 기록만 묻는데 두 주 전체를 셈 |
|
||||||
|
|
||||||
|
- 기준이 맞은 두 케이스(주말 · 기간 없음)는 정의가 맞아서가 아니라 **경계가 문제를 가려서** 맞았습니다. 보통 경우만 시험하면 이렇게 "잘 된다"고 믿게 됩니다.
|
||||||
|
- `source: live`의 666은 Lab 03에서 위험합니다. "고친 뒤 오늘 기록만" 보려고 해도 두 주 전체를 보게 되기 때문입니다. 경계 케이스를 테스트에 넣어 두면 이런 일을 미리 막습니다.
|
||||||
|
|
||||||
|
## 과제 1 · 날짜별 · 릴리스별 (8분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 두 주 합계를 **날짜로** 나눠 오류가 언제 몰렸는지 보고, **릴리스로** 나눠 새 버전의 답이 도움이 됐는지(다시 묻는 비율) 봅니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 가계부의 월 합계와 날짜별 칸.** 한 달 지출이 늘었다는 것만으로는 무엇을 줄일지 모릅니다. 날짜별 칸을 보면 "15일에 한 번 크게 썼다"가 보입니다. 새로 바꾼 장보기 앱도 "그 앱으로 산 날만" 따로 봐야 좋아졌는지 압니다. 과제 1은 보고서에 날짜별 칸과 버전별 칸을 더하는 실습입니다.
|
||||||
|
|
||||||
|
팀장이 요청했습니다.
|
||||||
|
|
||||||
|
> "둘째 주에 오류가 늘었다는데, 매일 그런 건지 어느 날만 그런 건지 알고 싶어요. 그리고 지난주에 새 버전(1.1.0)과 카나리(1.2.0)를 내보냈잖아요. 버전마다 사람들이 같은 걸 다시 묻는 비율도 봐 주세요."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 날짜별 | `by_day` — 날짜마다 `total` · `errors` · `error_rate`, 날짜순 ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 릴리스별 | `by_release` — `자동 답변`인 실행만, 릴리스마다 `answers` · `repeats` · `repeat_rate` (3절) |
|
||||||
|
| 기대 결과 | `cases/lab01-challenge.json`, `lab01.json`도 그대로 |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: 다시 묻는 비율을 왜 `자동 답변`인 실행만으로 셀까요? 1.2.0은 몇 건이었나요 — 그 비율을 믿어도 될까요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json -v # 고치기 전
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**묶어서 세기(group by).** 같은 칸(날짜, 릴리스)의 값이 같은 줄끼리 모아 각각 셉니다. 오늘 보고서의 지표 대부분은 "무엇으로 묶고, 무엇을 세고, 무엇으로 나누나"의 세 가지로 정해집니다.
|
||||||
|
|
||||||
|
| 지표 | 묶는 칸 | 세는 것 | 나누는 것 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `by_day` | `started_at` 앞 10자 | 실행 · 오류 | 그날 실행 |
|
||||||
|
| `by_release` | `release` (자동 답변만) | 답 · 다시 물음 | 그 릴리스의 답 |
|
||||||
|
|
||||||
|
**분모를 고르는 일.** "다시 묻는 비율"의 분모가 전체 실행이면, 답을 못 한 실행(담당자 연결 · 오류)도 들어가 버전의 **답의 질**과 상관없는 것이 섞입니다. 답을 준 실행만 분모로 해야 "그 버전의 답이 도움이 됐나"를 잽니다.
|
||||||
|
|
||||||
|
**표본 크기.** 1.2.0은 카나리(일부 사용자만)라 답이 8건뿐입니다. 8건 중 5건이면 62.5%지만, 몇 건만 달라져도 크게 바뀝니다. 비율 옆에는 늘 **건수**를 같이 봅니다 (74일차 카나리).
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/6, 고친 뒤 6/6. 10/26은 70건 중 오류 16, 날짜는 평일 10일.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 날짜순으로</summary>
|
||||||
|
|
||||||
|
묶은 결과는 객체(`{"2026-10-19": {...}, ...}`)입니다. 키를 정렬해서 만들면 화면에도 날짜순으로 나옵니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 묶는 칸 ② 거르는 조건(자동 답변만) ③ 세는 것 ④ 앞의 지표를 두기**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 by_day 와 by_release 를 더해 줘.
|
||||||
|
by_day: started_at 앞 ____①자(날짜)로 묶어 {"2026-10-26": {total, errors, error_rate}}, 날짜순.
|
||||||
|
by_release: route 가 "____②" 인 실행만 release 로 묶어 {answers, repeats, repeat_rate}.
|
||||||
|
repeats 는 repeat 가 ____③ 인 수, repeat_rate = repeats / answers (소수 4자리).
|
||||||
|
앞의 지표는 ____④ 둔다. 기대 결과 cases/lab01-challenge.json, cases/lab01.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① `started_at`(`2026-10-26T09:05:39`)에서 날짜까지가 몇 글자인지 셉니다.
|
||||||
|
- ② `route`의 세 값 중 "답을 준" 갈래입니다.
|
||||||
|
- ③ AGENTS.md 2절 `repeat`의 값 중 "다시 물었다"는 쪽.
|
||||||
|
- ④ 같은 파일에 더하는 것이라 예제 2의 지표를 어떻게 하나요? `lab01` 5/5를 지켜야 합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 날짜별 · 릴리스별
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 예제 2의 보고서
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 날짜별 · 월요일 · 0.1s · by_day.2026-10-26.total = null (expected 70); by_day.2026-10-26.errors = null (expected 16); by_day.2026-10-26.error_rate = null (expected 0.2286)
|
||||||
|
✘ 날짜별 · 목요일 · 0.1s · by_day.2026-10-29.total = null (expected 97); by_day.2026-10-29.errors = null (expected 5)
|
||||||
|
✘ 날짜는 10일 · 0.1s · by_day has none of ['"2026-10-19"']
|
||||||
|
✘ 릴리스별 다시 묻기 · 1.2.0 · 0.1s · by_release has none of ['"1.2.0": {"answers": 8, "repeats": 5, "repeat_rate": 0.625}']
|
||||||
|
✘ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s · by_release has none of ['"1.0.0": {"answers": 402, "repeats": 19, "repeat_rate": 0.0473}']
|
||||||
|
✘ 1.1.0 · 0.1s · by_release has none of ['"1.1.0": {"answers": 181, "repeats": 14, "repeat_rate": 0.0773}']
|
||||||
|
Passed 0/6
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- 예제 2의 보고서에는 `by_day` · `by_release` 칸이 아직 없습니다. 날짜 케이스는 `null`, 릴리스 케이스는 찾을 글자가 없다(`has none of`)고 합니다.
|
||||||
|
- 릴리스 케이스가 `equals`가 아니라 글자 찾기(`contains`)인 까닭: 키 `1.2.0`에 점이 있어 `by_release.1.2.0.answers`처럼 내려갈 수 없습니다. 그래서 JSON 글자 `"1.2.0": {"answers": 8, …}`를 통째로 찾습니다 (AGENTS.md 7절).
|
||||||
|
- 0/6은 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 날짜별 · 월요일 · 1.0s
|
||||||
|
✔ 날짜별 · 목요일 · 0.1s
|
||||||
|
✔ 날짜는 10일 · 0.2s
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.2.0 · 0.1s
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s
|
||||||
|
✔ 1.1.0 · 0.0s
|
||||||
|
Passed 6/6
|
||||||
|
(lab01 5/5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 볼 것 | 값 | 읽는 법 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 10/26 (월) | 70건 · 오류 16 · 0.2286 | 두 주 오류 21건 중 16건이 이날 |
|
||||||
|
| 10/29 (목) | 97건 · 오류 5 | 나머지 5건은 목요일 |
|
||||||
|
| 날짜 | 평일 10일 (10/24 · 25 없음) | 주말에는 실행이 없음 |
|
||||||
|
| 1.0.0 | 답 402 · 다시 물음 19 · 4.73% | 기준 버전 |
|
||||||
|
| 1.1.0 | 답 181 · 다시 물음 14 · 7.73% | 새 버전 — 다시 묻는 비율이 올랐음 |
|
||||||
|
| 1.2.0 (카나리) | 답 **8** · 다시 물음 5 · 62.5% | 비율은 높지만 8건뿐 |
|
||||||
|
|
||||||
|
- 팀장의 첫 질문 "매일 그런가, 어느 날만 그런가"의 답: **월요일 하루**(16건)와 목요일(5건)입니다. 둘째 주 다른 날에는 오류가 없습니다. 어느 **시간**인지는 Lab 02 예제 2에서 봅니다.
|
||||||
|
- 둘째 질문 "버전마다 다시 묻는 비율"의 답: 1.1.0이 1.0.0보다 높고(7.73% > 4.73%), 1.2.0은 62.5%지만 **8건으로는 판단하기 이릅니다.** 비율과 함께 건수(`answers`)를 꼭 보고합니다.
|
||||||
|
- 1.0.0 + 1.1.0 + 1.2.0의 답 = 402 + 181 + 8 = 591 = 두 주 `자동 답변` 수. 분모를 "답을 준 실행"으로 정했으니 맞습니다.
|
||||||
|
- `lab01` 5/5: 같은 파일에 칸을 더했지만 앞의 지표는 그대로입니다.
|
||||||
|
|
||||||
|
## 과제 2 · 어느 단계가 느린가 · 얼마 들었나 (10분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 실행 전체의 p95만으로는 **어디가** 느린지 모릅니다. 단계(FAQ 검색 · LLM 답)마다 시간을 재고, 두 주 동안 LLM에 얼마를 썼는지(비용 · 실행당 비용 · 호출 수)를 보고서에 더합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 택배의 구간별 시간.** 택배가 늦었다고 화내기 전에 배송 조회를 보면 "집하 → 허브 → 배송" 구간마다 시간이 찍혀 있습니다. 허브에서 하루를 묵었다면 기사님을 재촉해도 소용없습니다. 과제 2는 헬프데스크의 한 번의 실행을 구간(단계)으로 나눠 재고, 택배비(LLM 비용)도 함께 적는 실습입니다.
|
||||||
|
|
||||||
|
운영팀이 물었습니다.
|
||||||
|
|
||||||
|
> "p95가 2.5초쯤인데, FAQ 검색이 느린 건가요 LLM이 느린 건가요? 그리고 두 주에 LLM 비용이 얼마나 들었는지, 한 번에 얼마쯤인지도요."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 단계별 | `steps.faq_ms` · `steps.answer_ms` — 그 단계 값이 0보다 큰 실행만, `{runs, p50, p95}` ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 비용 | `cost_usd`(합, 소수 5자리) · `cost_per_run`(실행당, 소수 6자리) · `llm_calls`(합) |
|
||||||
|
| 기대 결과 | `cases/lab01-challenge2.json`, `lab01` · `lab01-challenge`도 그대로 |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: `잠시 후`로 끝난 실행은 FAQ도 LLM도 거치지 않아 두 단계 모두 0ms입니다. 이 실행들을 단계 p50에 넣으면 무슨 일이 생길까요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json -v # 고치기 전
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**단계(step) 시간.** 한 실행의 `duration_ms`는 여러 단계의 합입니다. 오늘 헬프데스크는 `faq_ms`(FAQ 검색)와 `answer_ms`(LLM 답)를 기록합니다. 전체가 느릴 때 어느 단계가 늘었는지 보면 **고칠 곳**이 정해집니다 (72일차 timeout, 75일차 비용).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Q(["질문"]) --> F["FAQ 검색<br/>faq_ms"] --> A{{"LLM 답<br/>answer_ms"}} --> R(["답"])
|
||||||
|
F -.->|없음| N["담당자 연결<br/>answer_ms 0"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**거치지 않은 단계는 뺀다.** `잠시 후`(20건)는 두 단계 모두 0, `담당자 연결`(34건)은 답 단계가 0입니다. 0을 넣으면 "빠른 실행"처럼 보여 단계의 진짜 시간이 작게 나옵니다. 그래서 단계마다 **값이 0보다 큰** 실행만 모으고, 몇 건을 모았는지(`runs`)도 함께 씁니다.
|
||||||
|
|
||||||
|
**기간을 나누면 다른 문제가 보인다.** 두 주 전체의 FAQ 단계 p95는 작지만, 하루만 떼어 보면 다를 수 있습니다. 케이스에 "월요일 · FAQ 단계가 느림"이 있는 까닭입니다.
|
||||||
|
|
||||||
|
**비용.** 기록마다 `cost_usd`(그 실행의 LLM 비용)가 있습니다. 합은 두 주 예산을, 실행당 비용은 "질문 하나에 얼마"를 말합니다 (75일차 비용 · 예산). 0건이면 나누지 않고 0.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/5, 고친 뒤 5/5. 테스트 줄 끝에 `596 calls · $0.078360`처럼 보이는 것은 응답의 `llm_calls` · `cost_usd`를 flow.py가 함께 보여 주는 것입니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 백분위 함수를 다시 쓰기</summary>
|
||||||
|
|
||||||
|
예제 2의 p50 · p95 계산(오름차순 · `ceil(p × n)`번째)을 단계 값에도 그대로 씁니다. 같은 정의를 두 번 쓰지 말고 같은 함수를 부르게 요청합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 단계 값을 모으는 조건 ② 비용 반올림 ③ 호출 수 ④ 앞의 테스트**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 steps 와 비용을 더해 줘.
|
||||||
|
steps: faq_ms · answer_ms 마다, steps 의 그 값이 ____① 실행만 모아 {runs, p50, p95} (백분위 정의는 앞과 같음, 없으면 runs 0 · p50 0 · p95 0).
|
||||||
|
cost_usd: 실행마다 cost_usd 의 합, 소수 ____②자리 반올림. cost_per_run: cost_usd / total, 소수 6자리 (0건이면 0).
|
||||||
|
llm_calls: 실행마다 llm_calls 의 ____③.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab01-challenge2.json, ____④ 도 그대로 통과.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① 위 "먼저 생각해 보기"의 답입니다. 거치지 않은 단계의 값은 얼마로 기록되나요?
|
||||||
|
- ② 3절의 `cost_usd` 줄. 기대값 0.07836의 자리 수를 셉니다.
|
||||||
|
- ③ 실행마다 `llm_calls`는 0 또는 1입니다. 두 주 전체로는?
|
||||||
|
- ④ 같은 파일을 고쳤으니 다시 돌려 볼 테스트 두 개.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 단계별 · 비용
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 과제 1까지의 보고서
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 두 주 · 단계별 p95 · 0.1s · steps.faq_ms.p95 = null (expected 39); steps.answer_ms.p95 = null (expected 2509); steps.answer_ms.p50 = null (expected 1179)
|
||||||
|
✘ 두 주 · 단계마다 잰 실행 수 · 0.1s · steps.faq_ms.runs = null (expected 646); steps.answer_ms.runs = null (expected 596)
|
||||||
|
✘ 월요일 · FAQ 단계가 느림 · 0.1s · steps.faq_ms.p95 = null (expected 1000); steps.answer_ms.p95 = null (expected 2482)
|
||||||
|
✘ 두 주 · 비용 · 0.1s · cost_usd = null (expected 0.07836); cost_per_run = null (expected 0.000118); llm_calls = null (expected 596)
|
||||||
|
✘ 빈 기간 · 비용 0 · 0.0s · cost_usd = null (expected 0); cost_per_run = null (expected 0); llm_calls = null (expected 0); steps.faq_ms.p95 = null (expected 0)
|
||||||
|
Passed 0/5
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- `steps` · `cost_usd` · `cost_per_run` · `llm_calls` 칸이 아직 없어 모두 `null`입니다. 기록 한 줄에는 `steps`와 `cost_usd`가 있지만, 보고서가 아직 세지 않았습니다.
|
||||||
|
- 0/5는 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01.json
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 두 주 · 단계별 p95 · 0.9s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 단계마다 잰 실행 수 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 월요일 · FAQ 단계가 느림 · 0.2s · 50 calls · $0.006450
|
||||||
|
✔ 두 주 · 비용 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 빈 기간 · 비용 0 · 0.0s · 0 calls · $0.000000
|
||||||
|
Passed 5/5
|
||||||
|
(lab01 5/5 · lab01-challenge 6/6)
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 볼 것 | 두 주 | 월요일만 | 읽는 법 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| FAQ 단계 p95 | **39ms** (646건) | **1000ms** | 평소 FAQ는 빠름. 월요일에는 timeout(1초)에 걸린 실행이 p95를 차지 |
|
||||||
|
| 답 단계 p50 · p95 | 1179 · **2509ms** (596건) | p95 2482ms | 평소 느린 것은 LLM 답 단계 |
|
||||||
|
| 비용 | 0.07836달러 | 0.00645달러 | 두 주 LLM 비용 |
|
||||||
|
| 실행당 비용 | 0.000118달러 | | 질문 하나에 약 0.0001달러 |
|
||||||
|
| LLM 호출 | 596 | 50 | 답을 준 실행마다 한 번 |
|
||||||
|
|
||||||
|
- **운영팀 질문의 답:** 평소 p95 2.5초의 대부분은 **LLM 답 단계**(p95 2509ms)입니다. FAQ 검색은 39ms입니다. 그런데 **월요일만** 떼어 보면 FAQ 단계 p95가 1000ms — 그날은 FAQ 검색이 문제였습니다. 같은 지표도 기간을 나누면 다른 고칠 곳이 보입니다 (Lab 02 · 03).
|
||||||
|
- **`runs` 646 · 596:** FAQ 단계는 `잠시 후` 20건을 뺀 646건, 답 단계는 답을 준 591건 + 답 도중 실패한 5건 = 596건입니다. 0인 값을 넣었다면 단계 p50이 작게 나왔을 것입니다.
|
||||||
|
- **테스트 줄 끝의 `596 calls · $0.078360`:** 응답에 `llm_calls` · `cost_usd`가 생기자 flow.py가 함께 보여 주는 것입니다 (75일차 기능). 빈 기간은 `0 calls · $0.000000`.
|
||||||
|
- `lab01` 5/5 · `lab01-challenge` 6/6: 앞의 지표는 그대로입니다.
|
||||||
|
|
||||||
|
## 막혔을 때
|
||||||
|
|
||||||
|
| 증상 | 할 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| `total`이 맞지 않음 | `Get logs`가 웹훅 본문의 `from` · `to`를 넘기는지 (직접 거르지 않음), `source` 기본값이 `all`인지 |
|
||||||
|
| `p95_ms`가 1~2 다름 | 오름차순 · `ceil(p × n)`번째(1부터 셈) · 오류 실행도 포함하는지 |
|
||||||
|
| `error_rate`가 0.03149… | 소수 4자리 반올림 |
|
||||||
|
| 빈 기간에 `null` | 0건이면 나누지 않고 0 |
|
||||||
|
| `by_route.오류`가 없음 | `route`가 `null`인 실행을 `"오류"`로 |
|
||||||
|
| `by_release`를 `equals`로 못 찾음 | 키에 점(`1.2.0`)이 있어 케이스는 `contains`로 JSON 글자를 찾음 — 모양을 정확히 `{"answers", "repeats", "repeat_rate"}` 순서로 |
|
||||||
|
| 단계 p50이 너무 작음 | 0인 단계 값을 뺐는지 |
|
||||||
|
| 계속 안 됨 | `cp reference/lab01-example2-report.json my-work/log-report.json` 후 push (예제 2까지) |
|
||||||
|
|
||||||
|
## 확인
|
||||||
|
|
||||||
|
- [ ] 예제 1: `OPS.md` 1.1 (내 짐작 · 기준 보고서 · 다른 까닭).
|
||||||
|
- [ ] 예제 2: `cases/lab01.json` 5/5 (고치기 전 기준에 0/5).
|
||||||
|
- [ ] 예제 3: `cases/lab01-edges.json` 내 보고서 6/6, 기준이 ✘인 까닭을 `OPS.md` 1.2에.
|
||||||
|
- [ ] 과제 1: `cases/lab01-challenge.json` 6/6, `lab01`도 통과.
|
||||||
|
- [ ] 과제 2: `cases/lab01-challenge2.json` 5/5, `lab01` · `lab01-challenge`도 통과, `OPS.md` 2절.
|
||||||
|
- [ ] `my-work/PROMPTS.md`.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Lab 02 · 문제를 숫자로 찾고, 요약하기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-02-Q-FIND-PROBLEMS.md](LAB-02-Q-FIND-PROBLEMS.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 2026-10-31 정답 워크플로우 1회 실행 값입니다. 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 failures 와 unanswered_topics 를 더해 줘.
|
||||||
|
failures: status 가 error 인 실행을 (error_node, error_message) 로 묶어 [{node, message, count}], 많은 순.
|
||||||
|
unanswered_topics: route 가 "담당자 연결" 인 실행을 topic 으로 묶어 [{topic, count}], 많은 순.
|
||||||
|
담당자 연결은 status 가 success 라 failures 에 넣지 않는다. 앞의 지표는 그대로. 기대 결과 cases/lab02.json 앞 세 케이스.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `error_node` | 어느 노드에서 · 무슨 말로 실패했나가 원인의 단위 |
|
||||||
|
| ② | `담당자 연결` | 답을 못 해 사람에게 넘긴 실행 |
|
||||||
|
| ③ | `topic` | 무엇을 물었나로 묶어야 FAQ에 무엇이 없는지 보임 |
|
||||||
|
| ④ | 넣지 않는다 | `status`는 `success` — 오류가 아닌 실패. 따로 셈 |
|
||||||
|
|
||||||
|
정답: [reference/lab02-example1-problems.json](reference/lab02-example1-problems.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/09`) | **0/6** · 칸 없음 |
|
||||||
|
| 고친 뒤 (`answers/10`) | **3/6** (예제 1 셋 ✔) · `Search FAQ` timeout 16 · `Answer` 429 5 · 담당자 연결 와이파이 **34** |
|
||||||
|
|
||||||
|
**문제 목록 (`OPS.md` 3.1 예)**
|
||||||
|
|
||||||
|
| 순위 | 문제 | 숫자 | 영향 | 고칠 곳 · 누가 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | 와이파이 FAQ 없음 | 담당자 연결 34 (두 주), 답 못 함 5.11% | 사용자가 답을 못 받음 · 담당자 일 | FAQ 더하기 — 사람 (Lab 03) |
|
||||||
|
| 2 | FAQ 검색 timeout | 오류 16, 10/26 09시 16/16 | 그 시간 질문 모두 실패 | Search FAQ timeout · 재시도 — 에이전트 (Lab 03) |
|
||||||
|
| 3 | LLM 429 | 오류 5, 10/29 15시 5/7 | 그 시간 답 실패 | 사용량 한도 · 대체 경로 (75 · 72일차) |
|
||||||
|
|
||||||
|
- 풀이 때: 와이파이(34)를 1위로 둘 수 있는지 토론 — 오류율(3.15%)에는 안 보이지만 건수는 오류보다 많습니다.
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 anomalies 를 더해 줘.
|
||||||
|
started_at 앞 13자(예 2026-10-26T09)로 시간마다 묶어 {hour, total, errors, error_rate}.
|
||||||
|
실행이 5건 이상이고, error_rate 가 그 기간 전체 error_rate 의 3배 이상이고, 오류가 1건 이상인 시간만 시간순으로.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab02.json 전체.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `13` | `2026-10-26T09`까지 = 날짜 + 시 |
|
||||||
|
| ② | `5` | 표본이 작은 시간(1건 중 1건 실패 = 100%)을 뺌 |
|
||||||
|
| ③ | `3` | 그 기간 전체 오류율의 3배 — 평소보다 크게 나쁜 시간 |
|
||||||
|
| ④ | 시간순 | 케이스가 `anomalies.0` · `.1`을 순서대로 봄 |
|
||||||
|
|
||||||
|
정답: [reference/lab02-example2-anomalies.json](reference/lab02-example2-anomalies.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 | **3/6** (예제 1 뒤 그대로) |
|
||||||
|
| 고친 뒤 (`answers/11`) | **6/6** · 10/26 09시 16 · 16 · 1, 10/29 15시 7 · 5 · 0.7143, 첫 주 `[]`. `lab01-challenge2` 5/5 |
|
||||||
|
|
||||||
|
- 16 + 5 = 21 = 두 주 오류 전부 → "두 번의 사건". 09시 = FAQ timeout, 15시 = 429 (원인 묶음과 시간대가 1:1).
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/12`)
|
||||||
|
|
||||||
|
| 기간 | 전체 오류율 | 문턱 (× 3) | 이상 시간대 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 첫 주 | 0 | 0 | 없음 (오류 1건 이상 조건) |
|
||||||
|
| 둘째 주 | 0.0588 | 0.1764 | 09시 · 15시 |
|
||||||
|
| 월요일만 | 0.2286 | 0.6858 | 09시 |
|
||||||
|
| 목요일만 | 0.0515 | 0.1545 | 15시 |
|
||||||
|
| 두 주 | 0.0315 | 0.0945 | 둘 다 |
|
||||||
|
|
||||||
|
- **5/5.** 이 예제의 답: 문턱이 기간에 따라 일곱 배 넘게 달라짐. 사건이 비교 기준을 끌어올림 (월요일). 보고에는 기준 기간을 함께.
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 repeat_topics 를 더해 줘.
|
||||||
|
route 가 "자동 답변" 인 실행만 topic 으로 묶어 {topic, answers, repeats, repeat_rate}. repeats 가 0 인 주제는 뺀다.
|
||||||
|
정렬은 repeat_rate 가 높은 순, 같으면 repeats 가 많은 순. repeat_rate 는 repeats / answers, 소수 4자리.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab02-challenge.json, cases/lab02.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `topic` | 어느 주제의 답이 도움이 안 됐나 |
|
||||||
|
| ② | `0` | 다시 묻지 않은 주제는 문제 목록에서 뺌 |
|
||||||
|
| ③ | 높은 | 수(비밀번호 9)가 아니라 비율(프린터 9.88%)이 답의 질 |
|
||||||
|
| ④ | `answers` | 그 주제에 답한 수가 분모 (전체 실행 수가 아님) |
|
||||||
|
|
||||||
|
정답: [answers/77-lab02-challenge-repeat-topics.json](answers/77-lab02-challenge-repeat-topics.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/13`) | **1/5** · 회귀 케이스만 ✔ |
|
||||||
|
| 고친 뒤 (`answers/14`) | **5/5** · 프린터 81 · 8 · 0.0988, 비밀번호 139 · 9 · 0.0647, VPN 0.0645, 접수 현황 0.0625, 메일 0.0556, 설치 0.0328. `lab02` 6/6 |
|
||||||
|
|
||||||
|
- 팀장에게 답: **프린터 FAQ**부터 (비율 1위). 비밀번호는 수(9)로는 1위지만 많이 묻는 주제라서. 와이파이는 "답 못 함" 목록 쪽.
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-summary.json 을 새로 만들어 줘. 웹훅 POST my-log-summary, 이름 "77 Log summary (my)".
|
||||||
|
Get report: 지표를 다시 계산하지 말고 HTTP POST http://localhost:5678/webhook/my-log-report 로 부른다 (본문 from · to · tag · source 그대로, timeout 30000).
|
||||||
|
Write summary(AI Agent + OpenAI gpt-4.1-mini, temperature 0): 통계 중 total · errors · error_rate · failures · unanswered_topics · anomalies 만 JSON 으로 주고, 한국어 3문장으로
|
||||||
|
가장 많은 답 못 한 주제와 수 · 가장 많은 실패(노드 · 메시지 · 수) · 가장 나쁜 시간대(10월 26일 09시처럼). 통계에 없는 숫자와 짐작한 원인은 쓰지 않는다.
|
||||||
|
Check numbers(Code): 요약 글의 숫자를 모두 뽑아, 통계의 값 · 비율의 ×100 · 키와 글자 속 숫자(날짜 · 시간)에 있는지 본다. 없는 숫자는 unknown_numbers 에, 하나도 없으면 numbers_ok true.
|
||||||
|
결과 {summary, numbers_ok, unknown_numbers, top_topic, worst_hour}. 기대 결과 cases/lab02-challenge2.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `my-log-report` | 지표를 다시 쓰지 않고 이미 있는 보고서를 부름 — 정의가 한 벌 |
|
||||||
|
| ② | `3` | 팀장 회의용 세 문장 |
|
||||||
|
| ③ | `unknown_numbers` | 통계에 없는 숫자를 모아 보여 줌 (지어낸 숫자 찾기) |
|
||||||
|
| ④ | `true` | 모르는 숫자가 하나도 없을 때만 |
|
||||||
|
|
||||||
|
정답: [answers/77-lab02-challenge2-summary.json](answers/77-lab02-challenge2-summary.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/15`) | **0/3** · 404 |
|
||||||
|
| 고친 뒤 (`answers/16`) | **3/3** (1.7~3.4초) · 둘째 주 요약 "와이파이로 17건 … timeout of 1000ms exceeded … 16건 … 10월 26일 09시 … 16건 중 16건", `numbers_ok: true` |
|
||||||
|
|
||||||
|
- 풀이 때: 숫자 확인을 일부러 깨 보기 — 지시문에서 "통계에 있는 숫자만"을 빼거나 "대략적으로"를 넣으면 `unknown_numbers`에 무엇이 들어오나 (수업 준비 때는 재지 않음).
|
||||||
@@ -0,0 +1,673 @@
|
|||||||
|
# Lab 02 · 문제를 숫자로 찾고, 요약하기
|
||||||
|
|
||||||
|
- 관련 강의: [2단계 · 문제 찾기](../../Lecture/77/02-STAGE2-FIND-PROBLEMS.md)
|
||||||
|
- 소요 시간: 50분 (4교시 한 시간) · 예제 1 12분 · 예제 2 10분 · 예제 3 6분 · 과제 1 10분 · 과제 2 12분
|
||||||
|
- 준비: Lab 01의 `my-work/log-report.json` (없으면 `cp reference/lab01-example2-report.json my-work/log-report.json` 후 push. 날짜별 · 단계별은 Lab 01 과제까지 끝낸 것)
|
||||||
|
- 목표: 지표에서 **어디를 먼저 고칠지** 정할 근거를 찾는다. 실패를 원인별로 묶고, 답하지 못한 질문의 주제를 세고, 평소와 다른 시간대를 찾고, 그 "평소"가 기간에 따라 어떻게 바뀌는지 잰다. 답했는데도 다시 묻는 주제를 찾고, 마지막으로 결과를 LLM이 글로 설명하되 **숫자는 Code가 확인**한다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
E1["예제 1<br/>실패 묶음 ·<br/>답 못 한 주제"] --> E2["예제 2<br/>이상 시간대"] --> E3["예제 3<br/>기간을 바꾸면"] --> T1["과제 1<br/>다시 묻는 주제"] --> T2["과제 2<br/>LLM 요약 ·<br/>숫자 확인"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 복사해 붙일 요청문은 없습니다. 막히면 `▶` 를 하나씩 펼칩니다. 오늘 Lab 02는 모두 `my-log-report`에 칸을 더하고(과제 2만 새 파일), 기대 숫자는 두 주 기록에서 늘 같습니다.
|
||||||
|
|
||||||
|
## 예제 1 · 실패를 묶고, 답하지 못한 주제를 세기 (12분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 오류 21건을 하나씩 보지 않고 **원인(노드 · 메시지)으로 묶어** 몇 개의 문제인지 세고, 오류는 아니지만 사용자에게는 실패인 `담당자 연결`을 **주제로 묶어** 무엇이 없는지 봅니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 반품 사유와 "찾는 물건 없음" 메모.** 가게에 반품이 21건 들어왔을 때 한 건씩 보면 21가지 일입니다. 사유로 묶으면 "사이즈 16건, 불량 5건" — 고칠 일은 둘입니다. 그리고 반품은 아니지만 손님이 "○○ 있어요?" 하고 빈손으로 간 메모가 34장인데 모두 같은 물건이라면, 그 물건을 들여놓는 것이 가장 큰 일입니다. 예제 1은 보고서에 반품 사유표와 "찾는 물건 없음" 표를 더하는 실습입니다.
|
||||||
|
|
||||||
|
| 더하는 칸 | 묶는 것 | 묶는 기준 | 답하는 질문 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `failures` | `status`가 `error`인 실행 | (`error_node`, `error_message`) | 무엇이 · 어디서 · 몇 번 고장 났나 |
|
||||||
|
| `unanswered_topics` | `route`가 `담당자 연결`인 실행 | `topic` | 무엇을 물었는데 답을 못 했나 |
|
||||||
|
|
||||||
|
**순서.** 지금 보고서에 `lab02` 테스트를 먼저 돌려 0/6을 봄 → 요청문 → push → 앞 세 케이스(예제 1) ✔, 뒤 셋(예제 2)은 아직 ✘.
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 고치는 단위는 "건"이 아니라 "원인"이라는 것, 오류율에 안 잡히는 실패(답 못 함)가 따로 있다는 것, 그리고 그 실패를 고칠 사람이 워크플로우 담당이 아닐 수도 있다는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 3.1 문제 목록 (순위 · 문제 · 숫자 · 영향 · 고칠 곳).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v # 고치기 전 (Lab 01까지의 보고서)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 파일 · 웹훅 | `my-work/log-report.json` · `my-log-report` (칸을 더함) |
|
||||||
|
| 정의 | [AGENTS.md](AGENTS.md) 3절 `failures` · `unanswered_topics`, 많은 순 |
|
||||||
|
| 기대 결과 | `cases/lab02.json` 앞 세 케이스 |
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**왜 묶나.** 같은 원인의 실패는 한 번 고치면 함께 사라집니다. 오류 21건을 21개의 일로 보면 우선순위를 정할 수 없지만, (노드, 메시지)로 묶으면 몇 개의 원인인지와 원인마다 몇 건인지가 보입니다. 가장 큰 묶음이 가장 먼저 고칠 곳입니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
E[("오류 21건")] --> G{"(error_node,<br/>error_message)로 묶기"}
|
||||||
|
G --> A["원인 A · n건"]
|
||||||
|
G --> B["원인 B · m건"]
|
||||||
|
H[("담당자 연결 34건")] --> T{"topic으로 묶기"}
|
||||||
|
T --> W["주제 · k건"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**오류가 아닌 실패.** `담당자 연결`은 `status`가 `success`입니다. 워크플로우는 정상적으로 "모르겠으니 사람에게 넘긴다"를 했습니다. 그래서 오류율에는 잡히지 않습니다. 그런데 사용자는 답을 받지 못했고, 담당자는 그만큼 일을 더 합니다. 이런 실패는 **따로 세야** 보입니다.
|
||||||
|
|
||||||
|
**고칠 곳이 다르다.** 오류 묶음은 대개 워크플로우나 바깥 서비스(timeout · 사용량 한도)를 고칩니다. 답 못 한 주제는 대개 **콘텐츠**(FAQ에 항목이 없음)를 고칩니다. 같은 문제 목록 안에서도 고칠 사람이 다릅니다 (Lab 03 예제 1).
|
||||||
|
|
||||||
|
**테스트 케이스 (예제 1 부분).**
|
||||||
|
|
||||||
|
| 케이스 | 기대 |
|
||||||
|
| --- | --- |
|
||||||
|
| 가장 많은 실패 | `failures.0` = `Search FAQ` · `timeout of 1000ms exceeded` · 16 |
|
||||||
|
| 두 번째 실패 | `failures.1` = `Answer` · 5 |
|
||||||
|
| 답하지 못한 주제 | `unanswered_topics.0` = 와이파이 · 34 |
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/6, 예제 1 뒤 3/6 (뒤 셋은 예제 2).
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 그 주제는 FAQ에 있나</summary>
|
||||||
|
|
||||||
|
답 못 한 주제가 나오면 FAQ에 있는지 직접 물어 봅니다: `curl -s 'localhost:8800/faq?q=와이파이'`. `results`가 비어 있다면, 워크플로우를 아무리 고쳐도 답할 수 없습니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 실패를 묶는 기준 ② 답 못 함의 조건 ③ 묶는 칸 ④ 둘을 섞지 않기**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 failures 와 unanswered_topics 를 더해 줘.
|
||||||
|
failures: status 가 error 인 실행을 (____①, error_message) 로 묶어 [{node, message, count}], 많은 순.
|
||||||
|
unanswered_topics: route 가 "____②" 인 실행을 ____③ 으로 묶어 [{topic, count}], 많은 순.
|
||||||
|
담당자 연결은 status 가 success 라 failures 에 ____④. 앞의 지표는 그대로. 기대 결과 cases/lab02.json 앞 세 케이스.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① 실행 기록 한 줄에서 "어느 노드에서" 실패했는지 적힌 칸입니다 (AGENTS.md 2절).
|
||||||
|
- ② `route`의 세 값 중 "사람에게 넘긴" 갈래.
|
||||||
|
- ③ "무엇을 물었나"가 적힌 칸.
|
||||||
|
- ④ 담당자 연결의 `status`는 무엇인가요? 그렇다면 `failures`에는?
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 실패 묶음 · 답 못 한 주제
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 숫자는 두 주 기록에서 늘 같습니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · Lab 01 과제 2까지의 보고서
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 예제 1 · 가장 많은 실패 · 0.1s · 596 calls · $0.078360 · failures.0.node = null (expected Search FAQ); failures.0.message = null (expected timeout of 1000ms exceeded); failures.0.count = null (expected 16)
|
||||||
|
✘ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360 · failures.1.node = null (expected Answer); failures.1.count = null (expected 5)
|
||||||
|
✘ 예제 1 · 답하지 못한 주제 · 0.1s · 596 calls · $0.078360 · unanswered_topics.0.topic = null (expected 와이파이); unanswered_topics.0.count = null (expected 34)
|
||||||
|
✘ 예제 2 · 이상 시간대 1 · 0.1s · … · anomalies.0.hour = null (expected 2026-10-26T09); …
|
||||||
|
✘ 예제 2 · 이상 시간대 2 · 0.1s · … · anomalies.1.hour = null (expected 2026-10-29T15); …
|
||||||
|
✘ 예제 2 · 첫 주에는 없음 · 0.0s · … · anomalies = null (expected [])
|
||||||
|
Passed 0/6
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- Lab 01의 보고서는 "얼마나"(실행 · 오류율 · p95 · 비용)는 말하지만 "무엇이 · 언제"는 말하지 않습니다. `failures` · `unanswered_topics` · `anomalies` 칸이 없어 모두 `null`입니다.
|
||||||
|
- 줄 끝의 `596 calls · $0.078360`은 Lab 01 과제 2에서 더한 `llm_calls` · `cost_usd`가 그대로 있다는 표시입니다.
|
||||||
|
- 0/6은 **예상한 결과**입니다. 앞 셋은 예제 1, 뒤 셋은 예제 2에서 고칩니다.
|
||||||
|
|
||||||
|
#### 고친 뒤 · 예제 1
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과, 두 주 응답의 해당 칸):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 예제 1 · 가장 많은 실패 · 0.9s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 답하지 못한 주제 · 0.3s · 596 calls · $0.078360
|
||||||
|
✘ 예제 2 · 이상 시간대 1 · 0.1s · … · anomalies.0.hour = null (expected 2026-10-26T09); …
|
||||||
|
✘ 예제 2 · 이상 시간대 2 · 0.1s · … · anomalies.1.hour = null (expected 2026-10-29T15); …
|
||||||
|
✘ 예제 2 · 첫 주에는 없음 · 0.1s · … · anomalies = null (expected [])
|
||||||
|
Passed 3/6
|
||||||
|
|
||||||
|
"failures": [ { "node": "Search FAQ", "message": "timeout of 1000ms exceeded", "count": 16 },
|
||||||
|
{ "node": "Answer", "message": "429 Rate limit reached for requests", "count": 5 } ],
|
||||||
|
"unanswered_topics": [ { "topic": "와이파이", "count": 34 } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 묶음 | 건수 | 뜻 | 고칠 곳 · 누가 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Search FAQ` · timeout of 1000ms | **16** | FAQ 검색이 1초 안에 답하지 않아 끊김 | 워크플로우 (timeout · 재시도) — 에이전트 |
|
||||||
|
| `Answer` · 429 Rate limit | 5 | LLM 사용량 한도에 걸림 | 사용량 · 대체 경로 (75 · 72일차) |
|
||||||
|
| 담당자 연결 · 와이파이 | **34** | FAQ에 와이파이 항목이 없음 | 콘텐츠 (FAQ 더하기) — 사람 |
|
||||||
|
|
||||||
|
- **오류 21건 = 원인 2개**(16 + 5). 21개의 일이 아니라 두 가지 일입니다. 가장 큰 묶음(16)이 먼저입니다.
|
||||||
|
- **답 못 함 34건 = 주제 1개.** 담당자에게 넘긴 34건이 **모두 와이파이**입니다. 오류율(3.15%)에는 하나도 잡히지 않았지만, 건수로는 오류(21)보다 많습니다. 사용자에게는 이것이 가장 큰 실패입니다.
|
||||||
|
- `curl -s 'localhost:8800/faq?q=와이파이'`의 `results`가 비어 있는 것을 확인하면, 이 문제는 워크플로우가 아니라 **FAQ 내용**을 고쳐야 한다는 것이 분명해집니다 (Lab 03 예제 1).
|
||||||
|
- 남은 ✘ 셋은 예제 2(이상 시간대)입니다.
|
||||||
|
|
||||||
|
## 예제 2 · 평소와 다른 시간대 (10분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 시간마다 오류율을 보고, **평소보다 크게 높은** 시간만 `anomalies`로 뽑습니다. 예제 1의 원인 묶음이 "무엇이", 이상 시간대가 "언제" 고장 났는지를 말합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 시간대별 매출표의 빈칸.** 하루 매출표를 시간대별로 보면, 평소 시간당 10건씩 팔리던 가게가 9시에만 0건이라면 그 시간에 무슨 일(카드 단말기 고장?)이 있었던 것입니다. 손님이 1명뿐인 새벽 시간의 0건은 이상하지 않고요. 예제 2는 "손님이 충분히 있었는데, 평소보다 훨씬 나빴던 시간"만 골라내는 실습입니다.
|
||||||
|
|
||||||
|
| 조건 | 왜 |
|
||||||
|
| --- | --- |
|
||||||
|
| 시간(`started_at` 앞 13자)으로 묶음 | `2026-10-26T09` = 10/26 9시대 |
|
||||||
|
| 실행 **5건 이상** | 1건 중 1건 실패(100%) 같은 작은 표본을 뺌 |
|
||||||
|
| 오류율 ≥ 그 기간 전체 오류율의 **3배** | 평소보다 "크게" 나쁜 시간만 |
|
||||||
|
| 오류 1건 이상 | 전체 오류율이 0일 때 3배(0) 조건만으로 다 뽑히는 것을 막음 |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 이상 탐지의 기본 모양(묶기 → 비교 기준 → 문턱)과, 문턱을 정하는 것도 정의라는 것. 예제 1의 원인과 시간대를 맞춰 보면 장애의 모습이 보인다는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 3.1 문제 목록에 시간대를 더합니다.
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 정의 | 실행 5건 이상 · 오류율 ≥ 전체 오류율의 3배 · 오류 1건 이상, 시간순 ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 기대 결과 | `cases/lab02.json` 전체 (6/6) |
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**이상(anomaly)이란.** "평소"와 비교해 크게 다른 것입니다. 그래서 이상 탐지에는 늘 세 가지가 필요합니다: **무엇으로 묶나**(시간), **평소는 무엇인가**(그 기간 전체 오류율), **얼마나 달라야 이상인가**(3배, 5건 이상). 셋 다 사람이 정하는 정의입니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
R[("기간의 실행")] --> H["시간마다 묶기<br/>total · errors · error_rate"]
|
||||||
|
R --> B["평소 = 기간 전체 오류율"]
|
||||||
|
H --> F{"5건 이상 ·<br/>평소 × 3 이상 ·<br/>오류 1건 이상"}
|
||||||
|
B --> F
|
||||||
|
F -->|예| A(["anomalies"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**원인과 시간을 맞춰 보기.** 예제 1의 원인 묶음은 둘(16 · 5)이었습니다. 이상 시간대가 둘이고 오류 수가 같다면, 두 원인이 각각 한 시간에 몰려 일어난 것입니다 — 하루 종일 조금씩이 아니라 **한 번의 사건**이었다는 뜻입니다.
|
||||||
|
|
||||||
|
**테스트 케이스 (예제 2 부분).**
|
||||||
|
|
||||||
|
| 케이스 | 기대 |
|
||||||
|
| --- | --- |
|
||||||
|
| 이상 시간대 1 | `2026-10-26T09` · 16건 · 오류 16 |
|
||||||
|
| 이상 시간대 2 | `2026-10-29T15` · 7건 · 오류 5 |
|
||||||
|
| 첫 주에는 없음 | `anomalies: []` |
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 왜 5건 이상</summary>
|
||||||
|
|
||||||
|
1건 중 1건 실패도 오류율 100%입니다. 표본이 작은 시간을 빼야 "진짜 이상"이 남습니다 (Lab 01 과제 1의 1.2.0 8건과 같은 질문).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 묶는 단위 ② 표본 크기 ③ 비교 기준의 배수 ④ 순서**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 anomalies 를 더해 줘.
|
||||||
|
started_at 앞 ____①자(예 2026-10-26T09)로 시간마다 묶어 {hour, total, errors, error_rate}.
|
||||||
|
실행이 ____②건 이상이고, error_rate 가 그 기간 전체 error_rate 의 ____③배 이상이고, 오류가 1건 이상인 시간만 ____④으로.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab02.json 전체.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① `2026-10-26T09:05:39`에서 "시"까지가 몇 글자인지 셉니다.
|
||||||
|
- ② · ③ 위 조건 표에 있습니다.
|
||||||
|
- ④ 테스트가 `anomalies.0`과 `anomalies.1`을 어떤 순서로 기대하나요?
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json
|
||||||
|
uv run tools/flow.py call my-log-report requests/two-weeks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 이상 시간대
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 예제 1의 보고서
|
||||||
|
|
||||||
|
고치기 전은 예제 1 고친 뒤와 같습니다: `cases/lab02.json` **3/6**, 뒤 세 케이스가 `anomalies = null`로 ✘. 시간마다 묶는 칸이 아직 없기 때문입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
uv run tools/flow.py test cases/lab01-challenge2.json
|
||||||
|
uv run tools/flow.py call my-log-report requests/two-weeks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과, 두 주 응답의 `anomalies`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 예제 1 · 가장 많은 실패 · 0.9s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 답하지 못한 주제 · 0.2s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 이상 시간대 1 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 이상 시간대 2 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 첫 주에는 없음 · 0.0s · 292 calls · $0.038550
|
||||||
|
Passed 6/6
|
||||||
|
(lab01-challenge2 5/5)
|
||||||
|
|
||||||
|
"anomalies": [ { "hour": "2026-10-26T09", "total": 16, "errors": 16, "error_rate": 1 },
|
||||||
|
{ "hour": "2026-10-29T15", "total": 7, "errors": 5, "error_rate": 0.7143 } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 이상 시간대 | 실행 · 오류 | 오류율 | 문턱 (0.0315 × 3 = 0.0945) | 예제 1의 원인과 맞추면 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 10/26 (월) 09시 | 16 · **16** | 100% | 넘음 | FAQ timeout 16건 |
|
||||||
|
| 10/29 (목) 15시 | 7 · 5 | 71.4% | 넘음 | 429 5건 |
|
||||||
|
|
||||||
|
- **16 + 5 = 21 = 두 주 오류 전부.** 오류가 두 주에 걸쳐 조금씩 난 것이 아니라 **두 번의 사건**이었습니다: 월요일 아침 9시에 FAQ 검색이 한 시간 동안 느렸고(그 시간의 질문 16개가 모두 실패), 목요일 오후 3시에 LLM 사용량 한도에 걸렸습니다.
|
||||||
|
- **첫 주 `[]`:** 첫 주는 오류가 0건이라 "오류 1건 이상" 조건에서 모두 빠졌습니다.
|
||||||
|
- 다른 시간에도 오류가 섞인 시간이 있었다면, 5건 미만이거나 문턱 아래라서 빠졌을 것입니다. 이 문턱이 "평소의 흔들림"과 "사건"을 가릅니다.
|
||||||
|
- `lab01-challenge2` 5/5: 단계 · 비용 칸은 그대로입니다. `OPS.md` 3.1의 문제 목록에 "언제"를 더합니다.
|
||||||
|
|
||||||
|
## 예제 3 · 기간을 바꾸면 이상이 바뀐다 (6분, 에이전트 없이)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 예제 2의 규칙("그 기간 전체 오류율의 3배")은 그대로 두고, **기간만** 바꿔 이상 시간대가 어떻게 달라지는지 잽니다. 고치는 실습이 아니라 비교 기준을 **재 보는** 실습입니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · "평소보다 많이 먹었다"의 평소.** 명절 연휴 동안 먹은 양을 기준으로 하면 오늘 저녁은 "적게" 먹은 것이고, 평일 기준이면 "많이" 먹은 것입니다. "평소보다 많다"는 말은 평소를 언제로 잡느냐에 달렸습니다. 예제 3은 같은 규칙으로 기간만 바꿔 보며, 평소(비교 기준)가 판정을 어떻게 바꾸는지 보는 실습입니다.
|
||||||
|
|
||||||
|
**상황.** 운영 회의에서 누구는 "이번 주", 누구는 "어제 하루", 누구는 "두 주"로 이상을 말합니다. 같은 보고서 · 같은 규칙인데 답이 다르면 서로 다른 말을 하게 됩니다. `cases/lab02-periods.json`이 다섯 기간을 묻습니다.
|
||||||
|
|
||||||
|
| 기간 | 그 기간 전체 오류율 | 문턱 (× 3) | 볼 것 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 첫 주 | ? | ? | 이상이 나오나 |
|
||||||
|
| 둘째 주 | ? | ? | 09시 · 15시 |
|
||||||
|
| 월요일만 | ? | ? | 15시(목요일)는 없어야 · 09시는? |
|
||||||
|
| 목요일만 | ? | ? | 15시만 |
|
||||||
|
| 두 주 | ? | ? | 둘 다 |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 비교 기준이 바뀌면 같은 시간이 이상이 되기도 하고 아니기도 한다는 것, 그래서 보고에는 **어느 기간을 기준으로** 했는지를 함께 적어야 한다는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 3.2 표 (기간 · 전체 오류율 · 문턱 · 이상 시간대) — 위 표의 `?`를 채웁니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-periods.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**비교 기준(baseline)의 선택.** "그 기간 전체"는 여러 선택 중 하나입니다. 지난주 같은 시간, 같은 요일 평균, 고정된 SLO(Lab 03) 등도 있습니다. 기간이 짧을수록 그 안의 사건 하나가 기준 자체를 끌어올립니다 — 하루 오류의 대부분이 한 시간에 몰리면, 그날 전체 오류율이 높아져 문턱도 높아집니다.
|
||||||
|
|
||||||
|
| 비교 기준 | 좋은 점 | 조심할 것 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 그 기간 전체 (오늘) | 계산이 쉬움 | 기간이 바뀌면 판정이 바뀜, 사건이 기준을 끌어올림 |
|
||||||
|
| 지난 기간 같은 시간 | 계절 · 요일을 맞춤 | 지난 기간에도 사건이 있었으면 |
|
||||||
|
| 고정 기준 (SLO) | 늘 같은 잣대 | 정하는 사람이 필요 (Lab 03 예제 2) |
|
||||||
|
|
||||||
|
**기대 결과.** 5/5. 각 케이스의 `-v` 응답에서 `error_rate`와 `anomalies`를 읽어 `OPS.md` 3.2를 채웁니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 첫 주에 이상이 하나도 없는 까닭</summary>
|
||||||
|
|
||||||
|
첫 주 전체 오류율이 0이면 문턱(× 3)도 0입니다. 그러면 모든 시간이 "문턱 이상"이 될 텐데, 왜 하나도 안 나올까요? 예제 2 조건 표의 마지막 줄을 봅니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 실행 결과 · 기간을 바꾸면
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
이 예제는 **고치지 않습니다.** 예제 2의 보고서에 기간만 바꿔 다섯 번 묻습니다.
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-periods.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 첫 주 · 오류 0 → 이상 없음 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 둘째 주 · 기준 0.0588 × 3 · 0.1s · 304 calls · $0.039810
|
||||||
|
✔ 월요일만 · 기준 0.2286 × 3 · 0.0s · 50 calls · $0.006450
|
||||||
|
✔ 목요일만 · 15시만 · 0.1s · 71 calls · $0.009420
|
||||||
|
✔ 두 주 · 기준 0.0315 × 3 · 0.1s · 596 calls · $0.078360
|
||||||
|
Passed 5/5
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (기간마다)**
|
||||||
|
|
||||||
|
| 기간 | 그 기간 전체 오류율 | 문턱 (× 3) | 이상 시간대 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 첫 주 | 0 | 0 | 없음 — 오류 1건 이상 조건 |
|
||||||
|
| 둘째 주 | 0.0588 | 0.1764 | 10/26 09시 · 10/29 15시 |
|
||||||
|
| 월요일만 | **0.2286** | **0.6858** | 10/26 09시 (100%만 넘음) |
|
||||||
|
| 목요일만 | 0.0515 | 0.1545 | 10/29 15시 |
|
||||||
|
| 두 주 | 0.0315 | 0.0945 | 둘 다 |
|
||||||
|
|
||||||
|
- **같은 규칙, 다른 문턱.** 문턱이 0.0945(두 주)에서 0.6858(월요일만)까지 일곱 배가 넘게 달라졌습니다. 월요일 하루만 보면 09시의 16건 오류가 그날 전체 오류율을 22.9%로 끌어올려, 문턱도 함께 올라갔습니다. 사건이 비교 기준 자체를 바꾼 것입니다.
|
||||||
|
- 이번 기록에서는 09시 · 15시가 워낙 뚜렷해(100% · 71%) 기간을 바꿔도 사라지지 않았습니다. 하지만 오류율이 30%쯤인 시간이 있었다면, 두 주 기준으로는 이상이고 월요일 기준으로는 아니었을 것입니다.
|
||||||
|
- **월요일만 보면** "하루 종일 22.9%"처럼 보이지만, 시간으로 나누면 09시 한 시간입니다. 보고에는 이상 시간대와 함께 **어느 기간을 기준으로** 했는지를 적습니다 (`OPS.md` 3.2).
|
||||||
|
|
||||||
|
## 과제 1 · 답했는데 다시 묻는 주제 (10분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 답은 했지만(`자동 답변`) 같은 사람이 10분 안에 같은 주제를 다시 물은(`repeat`) 비율을 **주제마다** 세어, 어느 주제의 답이 도움이 안 됐는지 찾습니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 교환하러 다시 온 손님.** 물건을 팔았는데(답함) 손님이 같은 날 다시 와서 "설명대로 했는데 안 돼요"라고 한다면, 판 것은 맞지만 설명서가 나쁜 것입니다. 많이 팔린 물건은 다시 오는 손님 **수**도 많으니, 몇 명 중 몇 명이 다시 왔는지 **비율**로 봐야 어느 설명서가 나쁜지 압니다. 과제 1은 주제별 "다시 온 손님 비율"을 세는 실습입니다.
|
||||||
|
|
||||||
|
팀장이 물었습니다.
|
||||||
|
|
||||||
|
> "답을 못 한 건 이제 알겠어요. 그런데 답을 했는데도 사람들이 다시 묻는 주제는 어디예요? 답이 엉터리인 FAQ를 먼저 고치고 싶어요."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 칸 | `repeat_topics` — `자동 답변`만, 주제마다 `answers` · `repeats` · `repeat_rate`, 다시 물음이 0인 주제는 뺌 ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 순서 | `repeat_rate` 높은 순, 같으면 `repeats` 많은 순 |
|
||||||
|
| 기대 결과 | `cases/lab02-challenge.json`, `lab02`도 그대로 |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: 다시 물은 **수**가 가장 많은 주제와 다시 묻는 **비율**이 가장 높은 주제가 다르다면, 어느 쪽을 먼저 고칠까요? 와이파이는 이 표에 나올까요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json -v # 고치기 전
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**수와 비율.** 많이 묻는 주제는 다시 묻는 수도 많습니다. 수로 정렬하면 "인기 있는 주제" 목록이 되고, 비율로 정렬해야 "답이 도움이 안 된 주제" 목록이 됩니다. 둘을 나란히 보는 것이 이 과제의 핵심입니다.
|
||||||
|
|
||||||
|
| 정렬 | 답하는 질문 |
|
||||||
|
| --- | --- |
|
||||||
|
| `repeats` (수) | 다시 물어서 생긴 일이 가장 많은 곳은? (담당자의 일) |
|
||||||
|
| `repeat_rate` (비율) | 답의 질이 가장 나쁜 곳은? (FAQ 내용) |
|
||||||
|
|
||||||
|
**Lab 01 과제 1과 무엇이 다른가.** 그때는 **릴리스**로 묶어 "어느 버전의 답이 나빠졌나"를 봤고, 이번에는 **주제**로 묶어 "어느 내용의 답이 나쁜가"를 봅니다. 분모(답한 수)를 고르는 방법은 같습니다.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 1/5 (마지막 케이스 "앞의 지표는 그대로"만 ✔), 고친 뒤 5/5.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 마지막 케이스가 처음부터 ✔인 까닭</summary>
|
||||||
|
|
||||||
|
"앞의 지표는 그대로" 케이스는 `total` · `failures` · `anomalies`만 봅니다. 이미 있는 칸이라 고치기 전에도 ✔입니다. 고친 뒤에도 ✔여야 합니다 (회귀).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 묶는 칸 ② 빼는 주제 ③ 정렬 ④ 비율의 분모**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 repeat_topics 를 더해 줘.
|
||||||
|
route 가 "자동 답변" 인 실행만 ____① 으로 묶어 {topic, answers, repeats, repeat_rate}. repeats 가 ____② 인 주제는 뺀다.
|
||||||
|
정렬은 repeat_rate 가 ____③ 순, 같으면 repeats 가 많은 순. repeat_rate 는 repeats / ____④, 소수 4자리.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab02-challenge.json, cases/lab02.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① "무엇을 물었나"의 칸.
|
||||||
|
- ② 다시 묻지 않은 주제는 문제 목록에 필요 없습니다.
|
||||||
|
- ③ 위 "수와 비율" 표에서 이 과제의 질문은 어느 줄인가요?
|
||||||
|
- ④ Lab 01 과제 1의 `repeat_rate`와 같은 분모.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
uv run tools/flow.py test cases/lab02.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 다시 묻는 주제
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 예제 2의 보고서
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 두 주 · 다시 묻는 비율이 가장 높은 주제 · 0.1s · … · repeat_topics.0.topic = null (expected 프린터); repeat_topics.0.answers = null (expected 81); repeat_topics.0.repeats = null (expected 8); …
|
||||||
|
✘ 두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위 · 0.1s · … · repeat_topics.1.topic = null (expected 비밀번호); repeat_topics.1.repeats = null (expected 9); repeat_topics.1.repeat_rate = null (expected 0.0647)
|
||||||
|
✘ 두 주 · 다시 물은 주제는 6개 · 0.1s · … · repeat_topics.5.topic = null (expected 설치); repeat_topics.5.repeat_rate = null (expected 0.0328)
|
||||||
|
✘ 첫 주 · 설치는 다시 묻지 않음 · 0.0s · … · repeat_topics.0.topic = null (expected 프린터)
|
||||||
|
✔ 앞의 지표는 그대로 · 0.1s · 596 calls · $0.078360
|
||||||
|
Passed 1/5
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- `repeat_topics` 칸이 없어 넷은 `null`입니다.
|
||||||
|
- 마지막 "앞의 지표는 그대로"는 `total` 666 · `failures.0.count` 16 · `anomalies.0.hour`만 보는 회귀 케이스라, 이미 있는 칸으로 ✔입니다. 고친 뒤에도 ✔여야 합니다.
|
||||||
|
- 1/5는 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
uv run tools/flow.py test cases/lab02.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과, 두 주 응답의 `repeat_topics`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 두 주 · 다시 묻는 비율이 가장 높은 주제 · 0.9s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위 · 0.3s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 다시 물은 주제는 6개 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 첫 주 · 설치는 다시 묻지 않음 · 0.0s · 292 calls · $0.038550
|
||||||
|
✔ 앞의 지표는 그대로 · 0.1s · 596 calls · $0.078360
|
||||||
|
Passed 5/5
|
||||||
|
(lab02 6/6)
|
||||||
|
|
||||||
|
"repeat_topics": [ { "topic": "프린터", "answers": 81, "repeats": 8, "repeat_rate": 0.0988 },
|
||||||
|
{ "topic": "비밀번호", "answers": 139, "repeats": 9, "repeat_rate": 0.0647 },
|
||||||
|
{ "topic": "VPN", "answers": 124, "repeats": 8, "repeat_rate": 0.0645 },
|
||||||
|
{ "topic": "접수 현황", "answers": 96, "repeats": 6, "repeat_rate": 0.0625 },
|
||||||
|
{ "topic": "메일", "answers": 90, "repeats": 5, "repeat_rate": 0.0556 },
|
||||||
|
{ "topic": "설치", "answers": 61, "repeats": 2, "repeat_rate": 0.0328 } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 볼 것 | 수로 보면 | 비율로 보면 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1위 | 비밀번호 9건 | **프린터 9.88%** (81건 중 8건) |
|
||||||
|
| 비밀번호 | 1위 (답 139건으로 가장 많이 물음) | 2위 6.47% |
|
||||||
|
| 마지막 | 설치 2건 | 설치 3.28% |
|
||||||
|
|
||||||
|
- **팀장 질문의 답:** 답의 질이 가장 나쁜 FAQ는 **프린터**입니다. 비밀번호는 다시 묻는 수가 가장 많지만, 그만큼 많이 묻는 주제라서입니다 (139건). 비율로는 프린터 FAQ를 먼저 고칩니다.
|
||||||
|
- **와이파이가 없는 까닭:** 와이파이는 답을 못 했으니(`담당자 연결`) `자동 답변`이 아닙니다. "답 못 함"(예제 1)과 "답했는데 도움이 안 됨"(과제 1)은 서로 다른 실패 목록입니다.
|
||||||
|
- **첫 주에는 설치가 없음:** 첫 주에는 설치를 다시 물은 사람이 없어(0건) 목록에서 빠졌습니다. 두 주의 설치 2건은 모두 둘째 주입니다.
|
||||||
|
- `lab02` 6/6: 앞의 칸은 그대로입니다. 비율 옆 건수(81 · 139)를 `OPS.md` 3.3에 함께 적습니다.
|
||||||
|
|
||||||
|
## 과제 2 · LLM 요약, 숫자는 Code가 확인 (12분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 보고서의 통계를 LLM이 **세 문장**으로 설명하게 하고, 그 글에 나온 숫자가 모두 통계에 있는지 **Code가** 확인합니다. 숫자는 Code가, 글은 LLM이.
|
||||||
|
|
||||||
|
> **생활 속 예 · 점장의 세 줄 보고와 계산기.** 점장이 본사에 "이번 주 반품 21건, 대부분 월요일 오전"이라고 세 줄로 보고합니다. 본사는 그 숫자를 장부와 계산기로 맞춰 봅니다. 말솜씨가 좋아도 숫자가 장부에 없으면 믿을 수 없습니다. 과제 2는 LLM에게 세 줄 보고를 쓰게 하고, 그 숫자를 Code(계산기)가 장부(통계)와 맞춰 보는 실습입니다.
|
||||||
|
|
||||||
|
> "숫자 표는 운영팀만 봐요. 팀장 회의에 올릴 세 문장 요약을 자동으로 만들어 주세요. 대신 AI가 숫자를 지어내면 안 됩니다."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 파일 · 웹훅 | `my-work/log-summary.json` (새로) · `my-log-summary` ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 통계 | `my-log-report`를 **다시 불러** 받음 (지표를 또 계산하지 않음) |
|
||||||
|
| 요약 | LLM(`gpt-4.1-mini`)이 세 문장: 가장 큰 답 못 한 주제 · 가장 많은 실패 · 가장 나쁜 시간대 |
|
||||||
|
| 숫자 확인 | 요약의 숫자가 모두 통계에 있는지 Code가 → `numbers_ok`, `unknown_numbers` |
|
||||||
|
| 기대 결과 | `cases/lab02-challenge2.json` |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: 71일차 답 검사("답의 숫자가 규정에 있나")와 무엇이 같고 무엇이 다른가요? 5.88%와 0.0588은 같은 숫자인가요? "10월 26일 09시"의 26과 9는 어디에 있는 숫자인가요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge2.json -v # 고치기 전 (웹훅이 아직 없음)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**역할 나누기.** LLM은 글을 잘 쓰지만 세는 일과 숫자를 그대로 옮기는 일에 실수할 수 있습니다. 그래서 숫자는 Code가 세고(보고서), LLM은 **센 결과**만 받아 문장으로 바꾸고, 다시 Code가 문장의 숫자를 확인합니다. 숫자가 LLM을 거쳐 나왔다면 확인 없이는 쓰지 않습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
W(["from · to"]) --> R["Get report<br/>my-log-report 다시 부르기"] --> L{{"Write summary (LLM)<br/>통계 JSON → 세 문장"}} --> C["Check numbers (Code)<br/>문장의 숫자 ⊂ 통계의 숫자?"] --> O(["summary · numbers_ok ·<br/>unknown_numbers"])
|
||||||
|
R -.->|통계| C
|
||||||
|
```
|
||||||
|
|
||||||
|
**다시 부르기.** 요약 워크플로우가 지표를 다시 계산하면 정의가 두 벌이 됩니다. 한쪽만 고치면 숫자가 어긋납니다. 이미 있는 웹훅(`my-log-report`)을 HTTP로 부르면 정의는 늘 한 곳입니다 (74 · 76일차와 같은 방법).
|
||||||
|
|
||||||
|
**LLM에게 무엇을 주나.** 기록 666줄이 아니라 **필요한 통계만**(전체 · 실패 · 답 못 한 주제 · 이상 시간대) JSON으로 줍니다. 짧고, 싸고, LLM이 셀 일이 없습니다 (75일차 "보내는 것 줄이기").
|
||||||
|
|
||||||
|
**"있는 숫자"의 규칙.** 통계의 값(17, 16), 비율의 ×100(0.0588 → 5.88), 키와 글자 속 숫자(`2026-10-26T09`의 26 · 09, 메시지 속 1000)를 모읍니다. 요약에서 뽑은 숫자가 그 안에 모두 있으면 `numbers_ok: true`, 하나라도 없으면 그 숫자를 `unknown_numbers`에.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/3 (404), 고친 뒤 3/3. LLM 글은 실행마다 조금 다를 수 있지만 `numbers_ok`는 `true`여야 합니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 요약이 길거나 원인을 지어냄</summary>
|
||||||
|
|
||||||
|
지시문에 "정확히 세 문장, 통계에 있는 숫자만, 원인을 짐작하지 말 것, 다른 숫자를 더하지 말 것"을 씁니다. `temperature` 0.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 통계를 받는 곳 ② 문장 수와 내용 ③ 모르는 숫자를 모으는 칸 ④ 확인 결과**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-summary.json 을 새로 만들어 줘. 웹훅 POST my-log-summary, 이름 "77 Log summary (my)".
|
||||||
|
Get report: 지표를 다시 계산하지 말고 HTTP POST http://localhost:5678/webhook/____① 로 부른다 (본문 from · to · tag · source 그대로, timeout 30000).
|
||||||
|
Write summary(AI Agent + OpenAI gpt-4.1-mini, temperature 0): 통계 중 total · errors · error_rate · failures · unanswered_topics · anomalies 만 JSON 으로 주고, 한국어 ____②문장으로
|
||||||
|
가장 많은 답 못 한 주제와 수 · 가장 많은 실패(노드 · 메시지 · 수) · 가장 나쁜 시간대(10월 26일 09시처럼). 통계에 없는 숫자와 짐작한 원인은 쓰지 않는다.
|
||||||
|
Check numbers(Code): 요약 글의 숫자를 모두 뽑아, 통계의 값 · 비율의 ×100 · 키와 글자 속 숫자(날짜 · 시간)에 있는지 본다. 없는 숫자는 ____③ 에, 하나도 없으면 numbers_ok ____④.
|
||||||
|
결과 {summary, numbers_ok, unknown_numbers, top_topic, worst_hour}. 기대 결과 cases/lab02-challenge2.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① 지표를 다시 계산하지 않고 부르는 내 웹훅입니다.
|
||||||
|
- ② 팀장 회의용 요약은 몇 문장인가요?
|
||||||
|
- ③ AGENTS.md 3절 계약의 "로그 요약" 결과 칸 중 하나.
|
||||||
|
- ④ 모르는 숫자가 하나도 없을 때의 `numbers_ok`.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-summary.json
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
uv run tools/flow.py call my-log-summary requests/week2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`OPS.md` 3.4에 요약 하나와 `numbers_ok`를 적습니다.
|
||||||
|
|
||||||
|
### 실행 결과 · LLM 요약 · 숫자 확인
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다. LLM 글은 실행마다 조금 다를 수 있습니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 요약 웹훅이 아직 없음
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 요약 · 숫자는 통계에 있는 것만 · 0.0s · status 404 (expected 200); numbers_ok = null (expected true); top_topic = null (expected 와이파이); worst_hour = null (expected 2026-10-26T09)
|
||||||
|
✘ 요약 · 주제와 시간대를 말함 · 0.0s · status 404 (expected 200); summary has none of ['와이파이']
|
||||||
|
✘ 요약 · 날짜 · 0.0s · status 404 (expected 200); summary has none of ['10월 26일', '10-26', '26일']
|
||||||
|
Passed 0/3
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- `my-log-summary` 웹훅이 아직 없어 n8n이 404(찾을 수 없음)를 줬습니다. 모든 칸이 `null`, 요약 글도 없습니다.
|
||||||
|
- 셋째 케이스의 `['10월 26일', '10-26', '26일']`은 "셋 중 **하나라도** 있으면 ✔"입니다 (`contains`). LLM이 날짜를 어떻게 쓸지 몰라 여러 모양을 허용했습니다.
|
||||||
|
- 0/3은 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-summary.json
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
uv run tools/flow.py call my-log-summary requests/week2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과 `call`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 요약 · 숫자는 통계에 있는 것만 · 3.4s
|
||||||
|
✔ 요약 · 주제와 시간대를 말함 · 2.6s
|
||||||
|
✔ 요약 · 날짜 · 1.7s
|
||||||
|
Passed 3/3
|
||||||
|
|
||||||
|
HTTP 200 · 1.8s
|
||||||
|
{
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 요약에 나온 숫자 | 통계의 어디에 있나 |
|
||||||
|
| --- | --- |
|
||||||
|
| 17 (와이파이) | 둘째 주 `unanswered_topics.0.count` |
|
||||||
|
| 1000 | `failures.0.message` 글자 속 `timeout of 1000ms exceeded` |
|
||||||
|
| 16 (실패) · 16 · 16 | `failures.0.count` · `anomalies.0.total` · `anomalies.0.errors` |
|
||||||
|
| 10 · 26 · 09 | `anomalies.0.hour` 글자 `2026-10-26T09` 속 숫자 |
|
||||||
|
|
||||||
|
- 요약의 숫자 여섯 개가 모두 통계에 있어 `numbers_ok: true`, `unknown_numbers: []`입니다. 만약 LLM이 "약 20건"이나 "오류율 6%"라고 썼다면 20 · 6이 `unknown_numbers`에 들어가고 `numbers_ok`는 `false`가 됩니다.
|
||||||
|
- **둘째 주는 17건:** 두 주 전체 와이파이는 34건이고, 둘째 주만 물었으니 17건입니다. 요약은 보고서를 **같은 기간으로 다시 불러** 받은 숫자를 썼습니다. 정의가 한 곳(`my-log-report`)에만 있으니 숫자가 어긋나지 않습니다.
|
||||||
|
- **시간 1.7~3.4초:** 보고서(0.1초) + LLM 한 번입니다. 통계 JSON만 보냈으니 짧고 쌉니다.
|
||||||
|
- `top_topic` · `worst_hour`는 LLM이 아니라 Code가 통계에서 바로 뽑았습니다. 화면이나 다음 워크플로우(주간 보고)가 쓰는 값은 글이 아니라 이런 칸입니다.
|
||||||
|
|
||||||
|
## 막혔을 때
|
||||||
|
|
||||||
|
| 증상 | 할 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| `failures`의 순서가 다름 | 많은 순으로 정렬, 같은 수면 순서는 상관없음 |
|
||||||
|
| `unanswered_topics`에 오류가 섞임 | `route`가 `담당자 연결`인 것만 (`null`은 오류) |
|
||||||
|
| `anomalies`가 너무 많음 · 없음 | 5건 이상 · 3배 · 오류 1건 이상 세 조건, 비교 기준은 **그 기간의** 전체 오류율 |
|
||||||
|
| `repeat_topics`의 비율이 조금 다름 | 분모가 그 주제의 `자동 답변` 수인지 (전체 실행 수가 아님) |
|
||||||
|
| `numbers_ok`가 늘 false | `unknown_numbers`를 봅니다. 날짜 · 시간 · 백분율 · 메시지 속 숫자를 "있는 숫자"에 넣었는지 |
|
||||||
|
| 요약이 404 · timeout | `my-log-report`가 push돼 있는지, Get report의 timeout 30초 |
|
||||||
|
| 계속 안 됨 | `reference/lab02-example1-problems.json`, `reference/lab02-example2-anomalies.json` |
|
||||||
|
|
||||||
|
## 확인
|
||||||
|
|
||||||
|
- [ ] 예제 1: `cases/lab02.json` 앞 세 케이스, `OPS.md` 3.1 문제 목록.
|
||||||
|
- [ ] 예제 2: `cases/lab02.json` 6/6, 원인과 시간대를 맞춰 봤다.
|
||||||
|
- [ ] 예제 3: `cases/lab02-periods.json` 5/5, `OPS.md` 3.2 (기간마다 문턱).
|
||||||
|
- [ ] 과제 1: `cases/lab02-challenge.json` 5/5, 수와 비율의 1위가 다른 까닭을 `OPS.md` 3.3에.
|
||||||
|
- [ ] 과제 2: `cases/lab02-challenge2.json` 3/3, 요약 하나를 `OPS.md` 3.4에.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Lab 03 · 고치고, 다시 돌려 재고, 보고하기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-03-Q-IMPROVE-AND-REMEASURE.md](LAB-03-Q-IMPROVE-AND-REMEASURE.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 2026-10-31 정답 워크플로우 1회 실행 값입니다. 오늘 실습 기록의 시간(ms)은 실행마다 다릅니다. 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/helpdesk.json (제공 helpdesk-live 를 복사한 것) 의 Search FAQ 노드만 고쳐 줘.
|
||||||
|
options.timeout 을 1000 에서 3000 으로, 그리고 retryOnFail true, maxTries 2, waitBetweenTries 300 (parameters 밖, 노드 바로 아래).
|
||||||
|
그래도 실패하면 지금처럼 오류 출력(onError continueErrorOutput)으로 Failed → Log Failed 에 기록한다.
|
||||||
|
다른 노드와 로그 모양(steps 포함)은 그대로. 기대 결과 cases/replay-after.json (FAQ 가 1.2초 느린 동안 10/10), cases/lab03.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `3000` | 느릴 때 FAQ가 1.2초 — 1초에 끊으면 모두 실패. 로그의 평소 `faq_ms`는 20~40ms |
|
||||||
|
| ② | `true` | 잠깐의 끊김은 한 번 더 (72 · 76일차) |
|
||||||
|
| ③ | `continueErrorOutput` | 실패도 로그에 남겨야 다음 주 보고서에 보임 |
|
||||||
|
| ④ | 그대로 | 로그 모양이 바뀌면 보고서의 정의가 깨짐 |
|
||||||
|
|
||||||
|
정답: [reference/lab03-example1-helpdesk-fixed.json](reference/lab03-example1-helpdesk-fixed.json) · FAQ: `requests/faq-wifi.json` (사람이 `faq/add`)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/17`) | `replay-before` 10/10(200만 봄) · 기록 **10건 모두 오류** (`Search FAQ`, 약 1030ms) · `lab03` **3/4** ("답하지 못한 주제 없음"은 0건이라 우연히 ✔) |
|
||||||
|
| 고친 뒤 (`answers/18`) | `replay-after` **10/10** (모두 자동 답변, 와이파이 `CORP-WIFI`, 한 건 2.3~4.5초) · `lab03` **4/4** · `ops` before 10 · after 10 · FAQ-007 |
|
||||||
|
|
||||||
|
- 풀이 때: timeout만 · FAQ만 고쳤다면 각각 무엇이 ✘인가 (FAQ만: 10 오류, timeout만: 와이파이 5개 담당자 연결).
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 unanswered_rate 와 slo 를 더해 줘.
|
||||||
|
unanswered_rate = "담당자 연결" 수 / total (소수 4자리).
|
||||||
|
기준 limits 는 기본 error_rate 0.01 · p95_ms 3000 · unanswered_rate 0.02, 웹훅 본문에 slo 가 있으면 그 값으로 덮어쓴다 (본문은 $('Report webhook').first().json.body).
|
||||||
|
지표가 기준보다 크면 그 이름을 breaches 에 넣고, slo = {ok: breaches 가 비었으면 true, breaches, limits}.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab03-slo.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `0.01` | AGENTS.md 3절 기본 기준 (오류율 1%) |
|
||||||
|
| ② | `3000` | p95 3초 |
|
||||||
|
| ③ | 크면 | 같으면 지킨 것 (`>`) |
|
||||||
|
| ④ | 비었으면 true | 어긴 지표가 하나도 없을 때만 지킴 |
|
||||||
|
|
||||||
|
정답: [reference/lab03-example2-slo.json](reference/lab03-example2-slo.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/19`) | **0/4** · 칸 없음 |
|
||||||
|
| 고친 뒤 (`answers/20`) | **4/4** · 둘째 주 `[error_rate, unanswered_rate]` (0.0476), 첫 주 `[unanswered_rate]`, 기준 0.06이면 지킴. 고친 뒤 기록 오류 0 · 답 못 함 0, **p95 3560 > 3000 → `[p95_ms]`**. `lab02-challenge` 5/5 |
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/21`)
|
||||||
|
|
||||||
|
| 기록 | FAQ | 오류 | FAQ 단계 p50 · p95 | 답 단계 p50 | 전체 p95 | SLO |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `before` | 느림 | 10 | 1043 · 1098ms | 0 | 1098ms | `error_rate` |
|
||||||
|
| `after` | 느림 | 0 | 1238 · 1278ms | 1103ms | 3560ms | `p95_ms` |
|
||||||
|
| `normal` | 평소 | 0 | 17 · 39ms | 997ms | 2147ms | 지킴 |
|
||||||
|
|
||||||
|
- `replay-normal` **10/10** (0.8~2.3초), `lab03-cost` **4/4**. 이 예제의 답: 늘어난 시간은 모두 FAQ 단계 — timeout은 "견디기"이고 "빠르게"가 아님. 진짜 고칠 곳은 FAQ 검색이 느려지는 원인.
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/weekly.json 을 새로 만들어 줘. 웹훅 POST my-weekly, 본문 from · to · prev_from · prev_to, 이름 "77 Weekly report (my)".
|
||||||
|
This week · Last week: my-log-report 를 두 번 부른다 (이번 주 from · to, 지난주 prev_from · prev_to). Summary: my-log-summary 를 이번 주로 부른다 (timeout 60000).
|
||||||
|
Write report(Code)가 마크다운을 만든다. ## 숫자: | 지표 | 지난주 | 이번 주 | 표 (요청 · 오류율 · p95 · 답 못 함 · SLO), 비율은 0.0588 을 5.88%처럼.
|
||||||
|
## 문제: 실패 · 답하지 못한 질문 · 이상 시간대("2026-10-26 09시"). ## 요약: LLM 요약과 숫자 확인 결과.
|
||||||
|
## 다음 할 일: LLM 이 아니라 규칙으로 — 답 못 한 주제가 있으면 "FAQ에 '<주제>' 항목 추가", Search FAQ 실패가 있으면 timeout 점검, 429 가 있으면 사용량 한도 점검.
|
||||||
|
결과 {markdown, total, error_rate, slo_ok, numbers_ok}. 기대 결과 cases/lab03-challenge.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `my-log-summary` | Lab 02 과제 2의 요약을 다시 부름 (숫자 확인 포함) |
|
||||||
|
| ② | `5.88%` | 사람이 읽는 보고 — ×100, 소수 2자리 |
|
||||||
|
| ③ | 규칙 | 문제 → 할 일이 정해져 있으면 규칙이 늘 같게 씀. LLM은 요약만 |
|
||||||
|
| ④ | `numbers_ok` | 요약의 숫자 확인 결과를 보고와 함께 |
|
||||||
|
|
||||||
|
정답: [answers/77-lab03-challenge-weekly.json](answers/77-lab03-challenge-weekly.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/22`) | **0/7** · 404 |
|
||||||
|
| 고친 뒤 (`answers/23`) | **7/7** (1.7~3.6초) · 요청 309 → 357, 오류율 0% → 5.88%, p95 2525 → 2598ms, 답 못 함 5.5% → 4.76%, SLO 위반 1 → 2, 문제 다섯 줄, 할 일 셋 (규칙) |
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/slo-alert.json 을 새로 만들어 줘. 웹훅 POST my-slo-alert, 본문 from · to · tag · source · slo, 이름 "77 SLO alert (my)".
|
||||||
|
Get report: my-log-report 를 부른다 (본문 그대로). Recent alerts: POST http://data-api:8800/alerts/list {workflow: "slo", reason: "slo", within: 600}.
|
||||||
|
Decide(Code): breaches 가 없으면 alert "none", 있는데 10분 안의 같은 알림이 있으면 "suppressed", 아니면 "sent".
|
||||||
|
message 는 "SLO breach: error_rate 0.0588 > 0.01, …" 처럼 지표 · 값 · 기준. Send it?(If): sent 일 때만 POST /alerts {workflow: "slo", severity: "high", reason: "slo", breaches, message}.
|
||||||
|
두 갈래 모두 같은 결과 {ok, breaches, alert, message, total} 로 응답. 기대 결과 cases/lab03-challenge2.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `600` | 10분 = 600초 (76일차 알림 폭주 막기와 같음) |
|
||||||
|
| ② | `suppressed` | 보내지 않았다는 것도 결과에 남김 |
|
||||||
|
| ③ | `high` | SLO 위반은 급하지만 문의를 잃는 중은 아님 (76일차 등급) |
|
||||||
|
| ④ | 응답 | sent · suppressed · none 어느 갈래든 같은 모양 |
|
||||||
|
|
||||||
|
정답: [answers/77-lab03-challenge2-slo-alert.json](answers/77-lab03-challenge2-slo-alert.json)
|
||||||
|
|
||||||
|
| 10/31 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/24`) | **0/6** · 404, 알림 0 |
|
||||||
|
| 고친 뒤 (`answers/25`) | **6/6** · 둘째 주 `sent` → 다시 `suppressed` → 알림 1개, 첫 주 기준 0.06 `none`. 알림 `[high] slo · SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02`. `lab03-slo` 4/4 |
|
||||||
|
|
||||||
|
- 76일차 Lab 02 과제 2(알림 폭주 막기)와 같은 모양이라는 것을 짚습니다: 판정 → 최근 같은 알림 → 보낼지. 79일차 Ops Assistant로 이어집니다.
|
||||||
@@ -0,0 +1,830 @@
|
|||||||
|
# Lab 03 · 고치고, 다시 돌려 재고, 보고하기
|
||||||
|
|
||||||
|
- 관련 강의: [3단계 · 고치고 다시 재기](../../Lecture/77/03-STAGE3-IMPROVE-AND-REMEASURE.md)
|
||||||
|
- 소요 시간: 50분 (6교시 한 시간) · 예제 1 14분 · 예제 2 8분 · 예제 3 6분 · 과제 1 12분 · 과제 2 10분
|
||||||
|
- 준비: Lab 02의 `my-work/log-report.json` (없으면 `cp reference/lab02-example2-anomalies.json my-work/log-report.json` 후 push), 요약 `my-work/log-summary.json` (과제 1에서 씀)
|
||||||
|
- 목표: Lab 02에서 찾은 문제 둘(FAQ 검색 timeout, 와이파이 FAQ 없음)을 **고치고**, 같은 질문을 **다시 돌려서** 고친 효과를 숫자로 확인한다. 운영 기준(SLO)으로 판정하고, 고친 것의 대가(느려짐)를 재고, 주간 보고와 SLO 알림을 자동으로 만든다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
E1["예제 1<br/>고치기 전 → 고치기 →<br/>고친 뒤"] --> E2["예제 2<br/>SLO 판정"] --> E3["예제 3<br/>고친 것의 대가"] --> T1["과제 1<br/>주간 보고"] --> T2["과제 2<br/>SLO 알림"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 복사해 붙일 요청문은 없습니다. 막히면 `▶` 를 하나씩 펼칩니다. 오늘 실습에서 남기는 기록은 `source: live`이고 `tag`(`before` · `after` · `normal`)로 나눕니다. 연습이 끝나면 `uv run tools/flow.py ops --reset`.
|
||||||
|
|
||||||
|
## 예제 1 · 고치기 전 · 후를 같은 질문으로 (14분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 두 주 동안 쓰던 헬프데스크로 월요일 아침처럼 FAQ가 느린 상황을 **다시 만들어** 기록을 남기고(`before`), Lab 02의 두 문제를 고친 뒤 **같은 질문**을 다시 보내(`after`) 내 보고서로 전 · 후를 비교합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 카드 단말기 설정과 들여놓을 물건.** 월요일 아침 카드 단말기가 느려서 결제가 줄줄이 실패했고, 손님들은 없는 물건(와이파이 공유기)을 계속 찾았습니다. 단말기의 "기다리는 시간" 설정은 직원이 바꾸고, 없는 물건은 구매 담당이 들여놓습니다. 고친 뒤에는 **같은 손님 열 명**을 다시 받아 보고 정말 나아졌는지 장부로 확인합니다. 예제 1은 이 "고치기 전 → 고치기(두 사람) → 같은 손님으로 다시"를 하는 실습입니다.
|
||||||
|
|
||||||
|
| 단계 | 하는 일 | 남는 기록 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① 고치기 전 (에이전트 없이) | 제공 헬프데스크를 복사해 push, 느린 FAQ(1.2초)에서 질문 10개 | `tag before` 10줄 |
|
||||||
|
| ② 고치기 | 워크플로우: FAQ 검색 timeout · 재시도 (에이전트) · 데이터: 와이파이 FAQ 더하기 (나) | — |
|
||||||
|
| ③ 고친 뒤 | 같은 느린 FAQ에서 같은 10개 | `tag after` 10줄 |
|
||||||
|
| 비교 | 내 보고서에 `tag`로 물음 (`cases/lab03.json`) | — |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** 고친 효과는 **같은 조건 · 같은 질문**으로 다시 재야 알 수 있다는 것, 그리고 문제마다 고치는 곳(워크플로우 · 데이터)과 고치는 사람이 다르다는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 4.1 표 (고친 것 · 어디서 · 누가 · 고치기 전 · 고친 뒤).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ① 고치기 전 (에이전트 없이)
|
||||||
|
cp reference/helpdesk-live.json my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py push my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py test cases/replay-before.json -v # 느린 FAQ(1.2초)에서 10개, tag before
|
||||||
|
uv run tools/flow.py logs --tag before --source live --last 3
|
||||||
|
uv run tools/flow.py test cases/lab03.json -v # 보고서로 비교 (고친 뒤 기록은 아직 없음)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**다시 돌려 보기(replay).** 운영 기록으로 문제를 찾았다면, 그 상황을 **다시 만들어** 고치기 전과 후를 같은 조건에서 잽니다. 오늘은 고장 스위치(`fault faq slow`, 1.2초)로 월요일 아침의 느린 FAQ를 만들고, 같은 질문 10개를 보냅니다. 조건이 다르면(FAQ가 빠른 날 고친 뒤를 재면) 고친 효과인지 운인지 알 수 없습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 고치기 전
|
||||||
|
Q1(["질문 10개<br/>FAQ 1.2초"]) --> S1["Search FAQ<br/>timeout 1초"] -->|끊김| F1["Failed → 기록 (오류)"]
|
||||||
|
end
|
||||||
|
subgraph 고친 뒤
|
||||||
|
Q2(["같은 10개<br/>FAQ 1.2초"]) --> S2["Search FAQ<br/>timeout 3초 · 재시도"] --> A2{{"답 (LLM)"}} --> L2["기록 (자동 답변)"]
|
||||||
|
D[("FAQ + 와이파이")] -.-> S2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**두 문제는 고치는 곳이 다르다.**
|
||||||
|
|
||||||
|
| 문제 (Lab 02) | 고치는 곳 | 누가 | 왜 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| FAQ 검색이 1초에 끊김 (16건) | `my-work/helpdesk.json`의 Search FAQ | 에이전트 (요청문) | 워크플로우 설정 |
|
||||||
|
| 와이파이 FAQ가 없음 (34건) | 데이터 API의 FAQ (`requests/faq-wifi.json`) | 나 (`faq/add`) | 콘텐츠 — 워크플로우를 아무리 고쳐도 없는 답은 못 함 |
|
||||||
|
|
||||||
|
**`tag`로 나누기.** 헬프데스크는 본문의 `tag`를 기록에 그대로 남깁니다. 보고서에 `{"tag": "before", "source": "live"}`로 물으면 고치기 전 10줄만 셉니다. 두 주 기록(`seed`)과 섞이지 않게 `source: live`도 함께.
|
||||||
|
|
||||||
|
**`replay-before`가 10/10인 까닭.** 이 테스트는 질문을 보내고 "200이 오나"만 봅니다 (헬프데스크는 실패해도 "담당자에게 연결합니다"로 답함). 실패했는지는 **기록**과 **보고서**로 봅니다 — 사용자에게 보인 답과 실제로 일어난 일을 나눠 보는 것은 76일차와 같습니다.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전: 기록 10줄 모두 오류, `lab03` 3/4. 고친 뒤: `replay-after` 10/10 (모두 `자동 답변`, 와이파이 답에 `CORP-WIFI`), `lab03` 4/4.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · timeout은 얼마로</summary>
|
||||||
|
|
||||||
|
기록의 `faq_ms`(평소 20~40ms)와 느릴 때(1.2초)를 봅니다. 1초에 끊으면 1.2초짜리는 모두 실패합니다. 늘리면 오류는 줄지만 사용자는 그만큼 기다립니다 (72일차, 예제 3).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 2단계 · FAQ 더하기는 사람이</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -XPOST localhost:8800/faq/add -H 'Content-Type: application/json' -d @requests/faq-wifi.json
|
||||||
|
curl -s 'localhost:8800/faq?q=와이파이' # results에 FAQ-007이 나오나
|
||||||
|
```
|
||||||
|
워크플로우 안에서 `/faq/add`를 부르지 않습니다 (AGENTS.md 9절). 콘텐츠를 바꾸는 것은 사람의 결정입니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 새 timeout ② 재시도 ③ 실패했을 때의 갈래 ④ 나머지는 그대로**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/helpdesk.json (제공 helpdesk-live 를 복사한 것) 의 Search FAQ 노드만 고쳐 줘.
|
||||||
|
options.timeout 을 1000 에서 ____① 으로, 그리고 retryOnFail ____②, maxTries 2, waitBetweenTries 300 (parameters 밖, 노드 바로 아래).
|
||||||
|
그래도 실패하면 지금처럼 오류 출력(onError ____③)으로 Failed → Log Failed 에 기록한다.
|
||||||
|
다른 노드와 로그 모양(steps 포함)은 ____④. 기대 결과 cases/replay-after.json (FAQ 가 1.2초 느린 동안 10/10), cases/lab03.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① 느릴 때 FAQ가 1.2초 걸립니다. 넉넉하게, 그러나 너무 길지 않게 (72일차 timeout · 76일차 3초).
|
||||||
|
- ② 76일차 접수 건 만들기 노드의 재시도 설정과 같은 칸입니다.
|
||||||
|
- ③ 제공 파일의 Search FAQ에 이미 있는 설정입니다. 실패도 **기록에 남아야** 다음 주 보고서에 보입니다.
|
||||||
|
- ④ 로그 모양이 바뀌면 보고서의 정의가 깨집니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ② 고치기 (내가 하는 것)
|
||||||
|
curl -s -XPOST localhost:8800/faq/add -H 'Content-Type: application/json' -d @requests/faq-wifi.json
|
||||||
|
|
||||||
|
# ③ 고친 뒤
|
||||||
|
uv run tools/flow.py push my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py test cases/replay-after.json -v # 같은 10개, tag after (10/10 이어야)
|
||||||
|
uv run tools/flow.py test cases/lab03.json -v # 보고서로 전 · 후 비교
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · 고치기 전 · 고치기 · 고친 뒤
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 오늘 실습 기록의 시간(ms)은 실행마다 조금씩 다르고, 오류 수 · 갈래 · ✔ · ✘는 같아야 합니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 제공 헬프데스크 (FAQ timeout 1초, 와이파이 FAQ 없음)
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp reference/helpdesk-live.json my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py push my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py test cases/replay-before.json -v
|
||||||
|
uv run tools/flow.py logs --tag before --source live --last 3
|
||||||
|
uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄 · `logs`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", …, "slow_seconds": 1.2}
|
||||||
|
✔ before · 회의실 와이파이가 안 · 2.1s
|
||||||
|
✔ before · 사내 Wi-Fi 비밀번 · 1.2s
|
||||||
|
… (10개 모두 ✔, 1.1~2.1s)
|
||||||
|
Passed 10/10
|
||||||
|
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", …}
|
||||||
|
|
||||||
|
10 run(s) · errors 10 (100.0%)
|
||||||
|
route: 오류 10
|
||||||
|
2026-10-31T07:51:15 error None 1034ms 프린터 Search FAQ 프린터 출력이 안 돼요
|
||||||
|
2026-10-31T07:51:16 error None 1035ms 메일 Search FAQ 메일함 용량이 꽉 찼대요
|
||||||
|
2026-10-31T07:51:17 error None 1032ms 설치 Search FAQ 새 프로그램 설치는 어떻게 신청하나요?
|
||||||
|
|
||||||
|
✔ 고치기 전 · 10건 모두 실패 · 0.1s · 0 calls · $0.000000
|
||||||
|
✔ 고치기 전 · FAQ timeout · 0.1s · 0 calls · $0.000000
|
||||||
|
✘ 고친 뒤 · 오류 없음 · 0.1s · 0 calls · $0.000000 · total = 0 (expected 10); by_route.자동 답변 = null (expected 10)
|
||||||
|
✔ 고친 뒤 · 답하지 못한 주제 없음 · 0.0s · 0 calls · $0.000000
|
||||||
|
Passed 3/4
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- **`replay-before` 10/10인데 기록은 10건 모두 오류:** 테스트는 처음에 오늘 기록 · 더한 FAQ를 비우고, FAQ를 1.2초 느리게 만든 뒤(`before` 세 줄) 질문을 보냅니다. 헬프데스크는 실패해도 "지금은 답할 수 없어 담당자에게 연결합니다"로 **200을 줍니다.** 그래서 테스트는 ✔이지만, 기록에는 `error` · `Search FAQ` · 약 1030ms(1초 timeout + 처리)가 남았습니다. 사용자에게 보인 것과 실제로 일어난 일을 나눠 보는 까닭입니다.
|
||||||
|
- 끝의 `after` 줄은 테스트가 고장 스위치를 `ok`로 되돌린 것입니다.
|
||||||
|
- **`lab03` 3/4:** 내 보고서에 `{"tag": "before", "source": "live"}`로 물어 "10건 모두 실패 · 원인 FAQ timeout 10건"이 ✔. "고친 뒤 · 오류 없음"은 고친 뒤 기록이 아직 0건이라 ✘(`total = 0`). "답하지 못한 주제 없음"은 0건이라 목록이 비어 **우연히** ✔입니다 — 고친 뒤 기록이 생긴 다음에 의미가 있습니다.
|
||||||
|
- 이 10건은 Lab 02에서 찾은 월요일 아침(09시 16건 모두 FAQ timeout)을 그대로 다시 만든 것입니다. 예상한 결과.
|
||||||
|
|
||||||
|
#### 고친 뒤 · timeout 3초 · 재시도 + 와이파이 FAQ
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -XPOST localhost:8800/faq/add -H 'Content-Type: application/json' -d @requests/faq-wifi.json
|
||||||
|
uv run tools/flow.py push my-work/helpdesk.json
|
||||||
|
uv run tools/flow.py test cases/replay-after.json -v
|
||||||
|
uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄 · `ops`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
{"added": "FAQ-007", "size": 7}
|
||||||
|
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", …, "slow_seconds": 1.2}
|
||||||
|
✔ after · 회의실 와이파이가 안 · 4.5s
|
||||||
|
✔ after · 사내 Wi-Fi 비밀번 · 2.4s
|
||||||
|
✔ after · 와이파이가 자주 끊겨요 · 2.4s
|
||||||
|
… (10개 모두 ✔, 2.3~4.5s)
|
||||||
|
Passed 10/10
|
||||||
|
|
||||||
|
✔ 고치기 전 · 10건 모두 실패 · 0.1s · 0 calls · $0.000000
|
||||||
|
✔ 고치기 전 · FAQ timeout · 0.1s · 0 calls · $0.000000
|
||||||
|
✔ 고친 뒤 · 오류 없음 · 0.1s · 10 calls · $0.000000
|
||||||
|
✔ 고친 뒤 · 답하지 못한 주제 없음 · 0.0s · 10 calls · $0.000000
|
||||||
|
Passed 4/4
|
||||||
|
|
||||||
|
fault switch: all ok
|
||||||
|
live run logs: 20 (tag before 10 · tag after 10)
|
||||||
|
added FAQ: 1 (FAQ-007)
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 볼 것 | 고치기 전 (`before`) | 고친 뒤 (`after`) | 무엇이 바꿨나 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 오류 | 10 / 10 | **0** / 10 | timeout 3초 — 1.2초 걸리는 FAQ를 기다림 |
|
||||||
|
| 갈래 | 오류 10 | 자동 답변 10 | FAQ 결과를 받아 LLM이 답함 |
|
||||||
|
| 와이파이 질문 5개 | 오류 | 자동 답변 (`CORP-WIFI` · `사번`) | FAQ-007을 데이터에 더함 |
|
||||||
|
| 한 건 시간 | 1.1~2.1초 | **2.3~4.5초** | FAQ 1.2초를 기다린 뒤 LLM 답 |
|
||||||
|
|
||||||
|
- **두 고침이 다 필요했습니다.** timeout만 늘렸다면 와이파이 질문 5개는 FAQ에 없어 `담당자 연결`이 됐을 것이고("답하지 못한 주제 없음" ✘), FAQ만 더했다면 1초에 끊겨 10개 모두 오류였을 것입니다. 하나는 워크플로우(에이전트), 하나는 콘텐츠(나)입니다.
|
||||||
|
- **`replay-after` 10/10은 `replay-before`와 뜻이 다릅니다.** 이번 테스트는 200만이 아니라 `route: 자동 답변` · `status: success`(와이파이는 답에 `CORP-WIFI`까지)를 확인합니다.
|
||||||
|
- **`10 calls · $0.000000`:** 고친 뒤 10건은 LLM을 한 번씩 불렀습니다. 제공 헬프데스크는 기록에 비용을 0으로 남기므로 `$0.000000`입니다 (두 주 기록에는 비용이 있음).
|
||||||
|
- **대신 느려졌습니다** (2.3~4.5초). 예제 2의 SLO와 예제 3에서 이 대가를 잽니다.
|
||||||
|
|
||||||
|
## 예제 2 · SLO로 판정하기 (8분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 보고서에 `unanswered_rate`와 `slo`(기준을 넘은 지표 `breaches`, 지켰는지 `ok`)를 더해, 매주 "약속을 지켰나"를 숫자 하나로 답하게 합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · "30분 안에 배달" 약속.** 피자 가게가 "빠르게 배달합니다"라고만 하면 늦어도 할 말이 없습니다. "30분 안에, 아니면 할인"이라고 **숫자로 약속**하면 매일 지켰는지가 분명해집니다. 예제 2는 헬프데스크에 "오류율 1% · 95%는 3초 안 · 답 못 함 2%"라는 약속을 정하고, 보고서가 지켰는지 판정하게 하는 실습입니다.
|
||||||
|
|
||||||
|
| 지표 | 기본 기준 (limits) | 둘째 주 (Lab 01 · 02에서) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `error_rate` | 0.01 (1%) | 0.0588 |
|
||||||
|
| `p95_ms` | 3000 | 2598 |
|
||||||
|
| `unanswered_rate` | 0.02 (2%) | ? (이번에 더함) |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** SLO가 지표를 "좋다 · 나쁘다"에서 "지켰다 · 못 지켰다"로 바꾼다는 것, 기준은 본문으로 바꿀 수 있어야 한다는 것(누가 정하나), 그리고 고친 뒤에도 **다른 지표가 새로 어길 수 있다**는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 5절 SLO 표.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json -v # 고치기 전
|
||||||
|
```
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 기본 기준 | 오류율 0.01 · p95 3000ms · 답 못 함 0.02 ([AGENTS.md](AGENTS.md) 3절), 본문 `slo`로 바꿀 수 있음 |
|
||||||
|
| 결과 | `unanswered_rate`, `slo: {ok, breaches, limits}` |
|
||||||
|
| 기대 결과 | `cases/lab03-slo.json` |
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**SLO(Service Level Objective, 서비스 수준 목표).** 사용자가 겪는 품질을 지표 하나와 숫자 하나로 정한 약속입니다. "빠르면 좋다"가 아니라 "p95 3초 안"처럼 정하면, 매주 지켰는지가 분명해지고 무엇부터 고칠지(어긴 지표)도 정해집니다. 기준은 사용자 · 팀장 · 비용(75일차)이 함께 정합니다.
|
||||||
|
|
||||||
|
| 용어 | 뜻 | 오늘 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| SLI (지표) | 무엇을 재나 | `error_rate` · `p95_ms` · `unanswered_rate` |
|
||||||
|
| SLO (목표) | 얼마를 지키나 | 0.01 · 3000 · 0.02 |
|
||||||
|
| breach (위반) | 목표를 넘음 | `breaches`에 지표 이름 |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
M["보고서 지표"] --> C{"지표 > 기준?"}
|
||||||
|
L["limits<br/>기본값 + 본문 slo"] --> C
|
||||||
|
C -->|예| B["breaches에 이름"]
|
||||||
|
C -->|아니오| K["지킴"]
|
||||||
|
B --> S(["slo.ok = breaches가 비었나"])
|
||||||
|
K --> S
|
||||||
|
```
|
||||||
|
|
||||||
|
**기준을 바꿀 수 있게.** 기준은 바뀝니다 (분기마다, 팀마다). 워크플로우에 고정하지 않고 본문의 `slo`로 덮어쓸 수 있으면, 같은 보고서로 "지금 기준이면?"과 "새 기준이면?"을 둘 다 물을 수 있습니다.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/4, 고친 뒤 4/4. 그리고 고친 뒤 기록(`tag after`)의 `slo.breaches`에 무엇이 남는지 봅니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 같으면 지킨 것인가</summary>
|
||||||
|
|
||||||
|
"오류율 1% 이하"라면 정확히 0.01은 지킨 것입니다. 그래서 비교는 `>`(크면 위반)입니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① · ② 기본 기준 ③ 위반의 비교 ④ ok의 뜻**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/log-report.json 의 Compute metrics 에 unanswered_rate 와 slo 를 더해 줘.
|
||||||
|
unanswered_rate = "담당자 연결" 수 / total (소수 4자리).
|
||||||
|
기준 limits 는 기본 error_rate ____① · p95_ms ____② · unanswered_rate 0.02, 웹훅 본문에 slo 가 있으면 그 값으로 덮어쓴다 (본문은 $('Report webhook').first().json.body).
|
||||||
|
지표가 기준보다 ____③ 그 이름을 breaches 에 넣고, slo = {ok: breaches 가 ____④, breaches, limits}.
|
||||||
|
앞의 지표는 그대로. 기대 결과 cases/lab03-slo.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① · ② 위 조건 표와 AGENTS.md 3절의 `slo` 줄.
|
||||||
|
- ③ 위 ▶ 1단계.
|
||||||
|
- ④ 어긴 지표가 하나도 없을 때만 약속을 지킨 것입니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json -v
|
||||||
|
uv run tools/flow.py call my-log-report '{"tag": "after", "source": "live"}'
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 결과 · SLO 판정
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · Lab 02까지의 보고서
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 둘째 주 · SLO 못 지킴 · 0.1s · 304 calls · $0.039810 · slo.ok = null (expected false); unanswered_rate = null (expected 0.0476); slo has none of ['"error_rate"', '"unanswered_rate"']
|
||||||
|
✘ 첫 주 · 답 못 함만 · 0.1s · 292 calls · $0.038550 · slo.ok = null (expected false); slo.breaches = null (expected ["unanswered_rate"])
|
||||||
|
✘ 기준을 바꾸면 · 첫 주 · 0.1s · 292 calls · $0.038550 · slo.ok = null (expected true)
|
||||||
|
✘ 고친 뒤 · 오류 · 답 못 함 기준은 지킴 · 0.1s · 10 calls · $0.000000 · unanswered_rate = null (expected 0)
|
||||||
|
Passed 0/4
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- 보고서에 `unanswered_rate` · `slo` 칸이 없어 모두 `null`입니다. 지표(오류율 · p95)는 있지만 "약속을 지켰나"를 말하는 칸이 없습니다.
|
||||||
|
- 넷째 케이스가 `10 calls`인 것은 예제 1의 고친 뒤 기록(`tag after`, 10건)을 물었기 때문입니다.
|
||||||
|
- 0/4는 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/log-report.json
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json -v
|
||||||
|
uv run tools/flow.py call my-log-report '{"tag": "after", "source": "live"}'
|
||||||
|
uv run tools/flow.py test cases/lab02-challenge.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과, 고친 뒤 기록에 물은 응답의 일부):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 둘째 주 · SLO 못 지킴 · 0.9s · 304 calls · $0.039810
|
||||||
|
✔ 첫 주 · 답 못 함만 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 기준을 바꾸면 · 첫 주 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 고친 뒤 · 오류 · 답 못 함 기준은 지킴 · 0.2s · 10 calls · $0.000000
|
||||||
|
Passed 4/4
|
||||||
|
|
||||||
|
HTTP 200 · 0.0s
|
||||||
|
{ "total": 10, "errors": 0, "error_rate": 0, "p50_ms": 2342, "p95_ms": 3560,
|
||||||
|
"steps": { "faq_ms": { "runs": 10, "p50": 1238, "p95": 1278 }, "answer_ms": { "runs": 10, "p50": 1103, "p95": 2282 } },
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": { "ok": false, "breaches": [ "p95_ms" ], "limits": { "error_rate": 0.01, "p95_ms": 3000, "unanswered_rate": 0.02 } } }
|
||||||
|
(lab02-challenge 5/5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 기간 · 기록 | 오류율 | p95 | 답 못 함 | `breaches` |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 첫 주 | 0 | 2525 | 0.055 | `unanswered_rate` |
|
||||||
|
| 첫 주 · 기준 `unanswered_rate` 0.06 | 0 | 2525 | 0.055 | 없음 → `ok: true` |
|
||||||
|
| 둘째 주 | 0.0588 | 2598 | 0.0476 | `error_rate`, `unanswered_rate` |
|
||||||
|
| 고친 뒤 (`tag after`) | 0 | **3560** | 0 | **`p95_ms`** |
|
||||||
|
|
||||||
|
- **둘째 주는 두 약속을 어겼습니다:** 오류율 5.88%(기준 1%)와 답 못 함 4.76%(기준 2%). p95 2598ms는 3초 안입니다. Lab 02의 문제 둘(FAQ timeout · 와이파이)이 그대로 SLO 두 개에 대응합니다.
|
||||||
|
- **첫 주에도 답 못 함은 어겼습니다** (5.5%). 와이파이 FAQ는 첫 주에도 없었습니다. 기준을 0.06으로 바꾸면 지킨 것이 됩니다 — 같은 기록, 다른 약속. 기준을 바꾸는 것은 숫자 하나지만, **누가 · 왜** 바꾸는지가 중요합니다.
|
||||||
|
- **고친 뒤에는 새 약속을 어겼습니다.** 오류 0 · 답 못 함 0으로 두 약속은 지켰지만, p95가 **3560ms**로 3초를 넘었습니다. `steps`를 보면 FAQ 단계 p50이 1238ms — 느린 FAQ를 1.2초씩 기다렸기 때문입니다. 고침이 문제 하나를 다른 지표로 옮겼습니다 (예제 3).
|
||||||
|
- `lab02-challenge` 5/5: 앞의 칸은 그대로입니다.
|
||||||
|
|
||||||
|
## 예제 3 · 고친 것의 대가 (6분, 에이전트 없이)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 예제 2에서 고친 뒤 기록이 p95를 어긴 까닭을 **단계 시간**(Lab 01 과제 2의 `steps`)으로 확인합니다. FAQ를 평소대로 돌려 같은 10개를 한 번 더 보내(`normal`), 느린 FAQ에서의 고친 뒤와 비교합니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 단말기 대기 시간을 늘린 대가.** 카드 단말기가 느린 날 "기다리는 시간"을 늘렸더니 결제 실패는 사라졌지만, 손님마다 계산이 1초씩 길어져 줄이 늘었습니다. 단말기가 정상인 날에는 기다릴 일이 없어 빠릅니다. 고친 것이 문제를 없앤 것인지, 다른 곳으로 옮긴 것인지 구분해야 합니다. 예제 3은 같은 손님 열 명을 "단말기가 느린 날"과 "정상인 날"에 받아 보고 비교하는 실습입니다.
|
||||||
|
|
||||||
|
| 기록 (`tag`) | FAQ | 헬프데스크 | 볼 것 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `before` | 느림 (1.2초) | 고치기 전 (timeout 1초) | 오류 · FAQ 단계 시간 |
|
||||||
|
| `after` | 느림 (1.2초) | 고친 뒤 (timeout 3초) | 오류 0, 대신 FAQ 단계 시간은? |
|
||||||
|
| `normal` (이번에) | 평소 | 고친 뒤 | FAQ 단계 시간 · p95 |
|
||||||
|
|
||||||
|
**끝나면 알게 되는 것.** timeout을 늘린 것은 "느린 FAQ를 **견디는**" 고침이지 "**빠르게** 하는" 고침이 아니라는 것. 고친 뒤에는 새로 나빠진 것을 찾아 함께 보고해야 한다는 것.
|
||||||
|
|
||||||
|
**적을 곳.** `my-work/OPS.md` 4.2 표 (기록 · FAQ 상태 · 오류 · FAQ 단계 p50 · p95)와 "새로 나빠진 것과 그 까닭".
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/replay-normal.json -v # FAQ 평소, 같은 10개, tag normal
|
||||||
|
uv run tools/flow.py test cases/lab03-cost.json -v # 세 기록을 단계 시간으로 비교
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**전체 시간 = 단계 시간의 합.** 고친 뒤 한 번의 실행은 "FAQ를 기다린 시간 + LLM이 답한 시간"입니다. FAQ가 1.2초 느리면, 고친 헬프데스크는 그 1.2초를 **기다려 줍니다.** 그래서 오류는 사라지지만 전체 시간이 늘어 p95가 기준(3초)을 넘을 수 있습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph after["after · FAQ 느림"]
|
||||||
|
A1["FAQ ≈ 1.2초"] --> A2["답 ≈ 1초"]
|
||||||
|
end
|
||||||
|
subgraph normal["normal · FAQ 평소"]
|
||||||
|
N1["FAQ ≈ 수십 ms"] --> N2["답 ≈ 1초"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 범위(range)로 확인하나.** 오늘 실습 기록의 시간은 실행마다 다릅니다. 그래서 `lab03-cost`는 "FAQ 단계 p50이 1150~3500ms 사이"처럼 **범위**로 봅니다. 두 주 기록처럼 늘 같은 숫자가 아니기 때문입니다.
|
||||||
|
|
||||||
|
**진짜 고침은 어디에.** FAQ 검색이 느린 원인(데이터 API · 검색 서버)을 고치는 것은 다른 팀의 일입니다. 오늘의 timeout은 그동안 사용자에게 "실패" 대신 "늦은 답"을 주는 **임시 조치**입니다. 보고서에는 둘을 나눠 적습니다.
|
||||||
|
|
||||||
|
**기대 결과.** `replay-normal` 10/10, `lab03-cost` 4/4. 각 케이스의 `-v` 응답에서 `steps.faq_ms.p50`과 `p95_ms`를 읽어 `OPS.md` 4.2를 채웁니다.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 답 단계도 늘었나</summary>
|
||||||
|
|
||||||
|
`lab03-cost`의 마지막 케이스는 답 단계(`steps.answer_ms.p50`)를 봅니다. FAQ 속도와 상관없이 비슷하다면, 늘어난 시간은 모두 FAQ 단계에서 온 것입니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 실행 결과 · 고친 것의 대가
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다. 시간은 실행마다 다르므로 `lab03-cost`는 범위로 확인합니다.
|
||||||
|
|
||||||
|
이 예제는 **고치지 않습니다.** 예제 1에서 고친 헬프데스크를 FAQ가 평소일 때 한 번 더 돌리고, 세 기록을 비교합니다.
|
||||||
|
|
||||||
|
#### 평소 FAQ로 같은 10개 (`tag normal`)
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/replay-normal.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", …}
|
||||||
|
✔ normal · 회의실 와이파이가 안 · 2.3s
|
||||||
|
✔ normal · 사내 Wi-Fi 비밀번 · 1.1s
|
||||||
|
… (10개 모두 ✔, 0.8~2.3s)
|
||||||
|
Passed 10/10
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나:** FAQ가 평소대로 빠르니 고친 헬프데스크가 기다릴 일이 없습니다. 같은 질문이 `after`(2.3~4.5초)보다 빠르게 0.8~2.3초에 답을 받았습니다. 와이파이 FAQ도 그대로 있어 10개 모두 `자동 답변`입니다.
|
||||||
|
|
||||||
|
#### 세 기록을 단계 시간으로 비교
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-cost.json -v
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과 `ops`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 고치기 전 · 느린 FAQ · 1초에 끊김 · 0.1s · 0 calls · $0.000000
|
||||||
|
✔ 고친 뒤 · 느린 FAQ · 기다려서 답함 · 0.1s · 10 calls · $0.000000
|
||||||
|
✔ 고친 뒤 · 평소 FAQ · 기다릴 일이 없음 · 0.0s · 10 calls · $0.000000
|
||||||
|
✔ 고친 뒤 · 답 단계는 FAQ 속도와 상관없음 · 0.1s · 10 calls · $0.000000
|
||||||
|
Passed 4/4
|
||||||
|
|
||||||
|
fault switch: all ok
|
||||||
|
live run logs: 30 (tag before 10 · tag after 10 · tag normal 10)
|
||||||
|
added FAQ: 1 (FAQ-007)
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (기록마다, `-v` 응답에서 읽은 값)**
|
||||||
|
|
||||||
|
| 기록 | FAQ | 오류 | FAQ 단계 p50 · p95 | 답 단계 p50 | 전체 p95 | SLO |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `before` | 느림 | **10** | 1043 · 1098ms | 0 (답 못 함) | 1098ms | `error_rate` 어김 |
|
||||||
|
| `after` | 느림 | 0 | **1238** · 1278ms | 1103ms | **3560ms** | `p95_ms` 어김 |
|
||||||
|
| `normal` | 평소 | 0 | **17** · 39ms | 997ms | 2147ms | 지킴 |
|
||||||
|
|
||||||
|
- **늘어난 시간은 모두 FAQ 단계에서 왔습니다.** 답 단계는 1103ms · 997ms로 비슷합니다. `after`의 FAQ 단계 1238ms는 느린 FAQ의 1.2초를 그대로 기다린 시간입니다.
|
||||||
|
- **고친 것은 "견디기"였습니다.** timeout을 늘려 느린 FAQ에서도 실패하지 않게 했지만, 사용자는 그만큼 더 기다립니다. FAQ가 평소대로면 같은 헬프데스크가 17ms 만에 FAQ를 받고 SLO도 지킵니다. 진짜 고칠 곳은 FAQ 검색이 느려지는 원인(다른 팀)입니다.
|
||||||
|
- **`ops`의 30줄**은 세 기록이 `tag`로 나뉘어 있다는 표시입니다. 같은 헬프데스크 · 같은 질문이라도 `tag`가 다르니 보고서에서 섞이지 않습니다.
|
||||||
|
- `OPS.md` 4.2에 이 표와 "새로 나빠진 것: p95, 까닭: 느린 FAQ를 기다림"을 적습니다.
|
||||||
|
|
||||||
|
## 과제 1 · 주간 보고 (12분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 만든 보고서 · 요약을 **다시 불러** 매주 월요일 회의에 그대로 붙일 주간 보고(마크다운)를 만듭니다. 숫자 · 문제는 Code가, 요약은 LLM이, 다음 할 일은 규칙이 씁니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 주간 매출 보고서.** 가게 점장은 매주 "지난주 · 이번 주 매출표, 문제 세 가지, 세 줄 요약, 다음 주 할 일"을 본사에 보냅니다. 매주 같은 칸에 같은 방식으로 채우니 본사는 표만 보고도 비교합니다. "할 일"은 정해진 규칙(재고가 없으면 주문, 단말기 고장이면 수리 요청)대로 씁니다. 과제 1은 이 보고서를 자동으로 만드는 실습입니다.
|
||||||
|
|
||||||
|
> "매주 월요일 회의에 쓸 주간 보고를 자동으로 만들어 주세요. 지난주와 비교한 숫자, 문제 목록, 세 문장 요약, 다음 할 일. 그대로 붙여 넣을 수 있게 마크다운으로요."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 파일 · 웹훅 | `my-work/weekly.json` (새로) · `my-weekly` (입력 `from` · `to` · `prev_from` · `prev_to`) |
|
||||||
|
| 재료 | `my-log-report`(이번 주 · 지난주), `my-log-summary` — **만든 것을 다시 부른다** |
|
||||||
|
| 결과 | `markdown`(절: `## 숫자` · `## 문제` · `## 요약` · `## 다음 할 일`), `total`, `error_rate`, `slo_ok`, `numbers_ok` |
|
||||||
|
| 기대 결과 | `cases/lab03-challenge.json` |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: "다음 할 일"을 LLM이 쓸까요, 규칙이 쓸까요? 문제 목록에서 정해진 할 일로 이어지는 것은 무엇인가요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge.json -v # 고치기 전 (웹훅이 아직 없음)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**조립(composition).** 주간 보고는 새로 계산하지 않습니다. 이미 테스트로 확인한 웹훅 셋을 부르고(이번 주 보고서 · 지난주 보고서 · 요약), 받은 것을 표와 목록으로 **조립**만 합니다. 정의는 늘 한 곳이고, 보고서를 고치면 주간 보고도 함께 바뀝니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
W(["from · to ·<br/>prev_from · prev_to"]) --> T["This week<br/>my-log-report"] --> P["Last week<br/>my-log-report"] --> S["Summary<br/>my-log-summary"] --> C["Write report (Code)<br/>표 · 문제 · 요약 · 할 일"] --> O(["markdown"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**누가 무엇을 쓰나.**
|
||||||
|
|
||||||
|
| 절 | 누가 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 숫자 (표) | Code | 숫자는 보고서 그대로, 사람이 읽게 `5.88%` |
|
||||||
|
| 문제 | Code | 실패 · 답 못 한 주제 · 이상 시간대를 목록으로 |
|
||||||
|
| 요약 | LLM (+ 숫자 확인) | 사람의 말로 — 확인 결과를 함께 |
|
||||||
|
| 다음 할 일 | 규칙 (Code) | "와이파이가 답 못 함 1위 → FAQ 추가"처럼 정해진 연결 |
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/7 (404), 고친 뒤 7/7. 한 번 부르는 데 2~4초 (보고서 둘 + LLM 하나).
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 다시 쓰지 말고 부르기</summary>
|
||||||
|
|
||||||
|
지표 계산을 또 쓰면 정의가 두 벌이 됩니다. 이미 있는 웹훅을 HTTP로 부릅니다 (AGENTS.md 5절 "다른 내 웹훅을 부르기"). 세 웹훅이 모두 push돼 있어야 합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 2단계 · 표의 모양</summary>
|
||||||
|
|
||||||
|
`| 요청 | 309 | 357 |`처럼 지난주 · 이번 주를 나란히. 이상 시간대 `2026-10-26T09`는 사람이 읽게 `2026-10-26 09시`로.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 요약을 받는 곳 ② 비율의 모양 ③ 할 일을 쓰는 방법 ④ 결과 칸**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/weekly.json 을 새로 만들어 줘. 웹훅 POST my-weekly, 본문 from · to · prev_from · prev_to, 이름 "77 Weekly report (my)".
|
||||||
|
This week · Last week: my-log-report 를 두 번 부른다 (이번 주 from · to, 지난주 prev_from · prev_to). Summary: ____① 를 이번 주로 부른다 (timeout 60000).
|
||||||
|
Write report(Code)가 마크다운을 만든다. ## 숫자: | 지표 | 지난주 | 이번 주 | 표 (요청 · 오류율 · p95 · 답 못 함 · SLO), 비율은 0.0588 을 ____②처럼.
|
||||||
|
## 문제: 실패 · 답하지 못한 질문 · 이상 시간대("2026-10-26 09시"). ## 요약: LLM 요약과 숫자 확인 결과.
|
||||||
|
## 다음 할 일: LLM 이 아니라 ____③으로 — 답 못 한 주제가 있으면 "FAQ에 '<주제>' 항목 추가", Search FAQ 실패가 있으면 timeout 점검, 429 가 있으면 사용량 한도 점검.
|
||||||
|
결과 {markdown, total, error_rate, slo_ok, ____④}. 기대 결과 cases/lab03-challenge.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① Lab 02 과제 2에서 만든 웹훅.
|
||||||
|
- ② 사람이 읽는 보고입니다. 0.0588을 백분율로 (소수 2자리).
|
||||||
|
- ③ 위 "누가 무엇을 쓰나" 표의 마지막 줄.
|
||||||
|
- ④ AGENTS.md 3절 "주간 보고" 결과 칸 중 요약의 확인 결과.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/weekly.json
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
uv run tools/flow.py call my-weekly requests/weekly.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`markdown`을 `OPS.md` 6절에 붙입니다.
|
||||||
|
|
||||||
|
### 실행 결과 · 주간 보고
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다. 요약 글은 실행마다 조금 다를 수 있습니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · 주간 보고 웹훅이 아직 없음
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✘ 주간 보고 · 숫자 · 0.0s · status 404 (expected 200); total = null (expected 357); error_rate = null (expected 0.0588); slo_ok = null (expected false); numbers_ok = null (expected true)
|
||||||
|
✘ 주간 보고 · 절 · 0.0s · status 404 (expected 200); markdown has none of ['## 숫자']
|
||||||
|
✘ 주간 보고 · 문제 절 · 0.0s · status 404 (expected 200); markdown has none of ['## 문제']
|
||||||
|
✘ 주간 보고 · 다음 할 일 · 0.0s · status 404 (expected 200); markdown has none of ['## 다음 할 일']
|
||||||
|
✘ 주간 보고 · 비교 숫자 · 0.0s · status 404 (expected 200); markdown has none of ['| 요청 | 309 | 357 |']
|
||||||
|
✘ 주간 보고 · 이상 시간대 · 0.0s · status 404 (expected 200); markdown has none of ['2026-10-26 09시']
|
||||||
|
✘ 주간 보고 · 와이파이 · 0.0s · status 404 (expected 200); markdown has none of ["FAQ에 '와이파이' 항목 추가"]
|
||||||
|
Passed 0/7
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- `my-weekly` 웹훅이 아직 없어 404입니다. 일곱 케이스가 숫자(`total` · `error_rate` · `slo_ok` · `numbers_ok`) 하나와, 마크다운의 절 · 표 줄 · 시간 표기 · 할 일 문장 여섯을 봅니다.
|
||||||
|
- 0/7은 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/weekly.json
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
uv run tools/flow.py call my-weekly requests/weekly.json
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과 `markdown`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ 주간 보고 · 숫자 · 3.6s
|
||||||
|
✔ 주간 보고 · 절 · 1.9s
|
||||||
|
✔ 주간 보고 · 문제 절 · 2.4s
|
||||||
|
✔ 주간 보고 · 다음 할 일 · 1.7s
|
||||||
|
✔ 주간 보고 · 비교 숫자 · 2.0s
|
||||||
|
✔ 주간 보고 · 이상 시간대 · 1.8s
|
||||||
|
✔ 주간 보고 · 와이파이 · 1.7s
|
||||||
|
Passed 7/7
|
||||||
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)
|
||||||
|
|
||||||
|
## 숫자
|
||||||
|
|
||||||
|
| 지표 | 지난주 | 이번 주 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 요청 | 309 | 357 |
|
||||||
|
| 오류율 | 0% | 5.88% |
|
||||||
|
| p95 | 2525ms | 2598ms |
|
||||||
|
| 답 못 함 | 5.5% | 4.76% |
|
||||||
|
| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |
|
||||||
|
|
||||||
|
## 문제
|
||||||
|
|
||||||
|
- 실패 16건 · Search FAQ · timeout of 1000ms exceeded
|
||||||
|
- 실패 5건 · Answer · 429 Rate limit reached for requests
|
||||||
|
- 답하지 못한 질문 17건 · 와이파이
|
||||||
|
- 이상 시간대 2026-10-26 09시 · 16/16 실패
|
||||||
|
- 이상 시간대 2026-10-29 15시 · 5/7 실패
|
||||||
|
|
||||||
|
## 요약 (LLM, 숫자 확인 통과)
|
||||||
|
|
||||||
|
가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 "timeout of 1000ms exceeded" 메시지로 16건입니다. 가장 심각한 이상 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.
|
||||||
|
|
||||||
|
## 다음 할 일
|
||||||
|
|
||||||
|
- FAQ에 '와이파이' 항목 추가
|
||||||
|
- FAQ 검색 timeout · 재시도 설정 점검 (72일차)
|
||||||
|
- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 절 | 어디서 왔나 | 이 보고에서 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 숫자 | 이번 주 · 지난주 `my-log-report` | 요청 309 → 357, 오류율 0% → 5.88% (`0.0588` → `5.88%`) |
|
||||||
|
| 문제 | 이번 주 보고서의 `failures` · `unanswered_topics` · `anomalies` | Lab 02에서 찾은 다섯 줄 그대로 |
|
||||||
|
| 요약 | `my-log-summary` (숫자 확인 통과) | 17 · 16 · 10월 26일 09시 |
|
||||||
|
| 다음 할 일 | 규칙 | 와이파이가 답 못 함 1위 → FAQ 추가, `Search FAQ` 실패 → timeout, 429 → 사용량 한도 |
|
||||||
|
|
||||||
|
- **지난주와 나란히 보니** 이번 주가 무엇이 나빠졌는지 바로 보입니다: 오류율이 0에서 5.88%로, SLO 위반이 하나에서 둘로. 답 못 함은 5.5%에서 4.76%로 조금 줄었지만 여전히 기준(2%) 위입니다.
|
||||||
|
- **다음 할 일 셋은 LLM이 쓴 것이 아닙니다.** 문제 목록에서 정해진 할 일로 이어지는 연결을 규칙으로 썼으니, 매주 같은 문제에는 같은 할 일이 나옵니다. 그리고 그 셋이 바로 Lab 03 예제 1에서 한 일(와이파이 FAQ · timeout)과 남은 일(429)입니다.
|
||||||
|
- **2~4초:** 보고서 두 번(각 0.1초) + 요약(LLM 한 번). 요약 글은 Lab 02 과제 2와 조금 다르지만 숫자 확인은 통과했습니다.
|
||||||
|
|
||||||
|
## 과제 2 · SLO를 어기면 알림 (10분)
|
||||||
|
|
||||||
|
### 이 실습의 의도
|
||||||
|
|
||||||
|
**한 줄로:** 기간을 받아 보고서의 SLO를 확인하고, **어겼으면** 76일차 알림 API에 high 알림을 남기되, 같은 알림을 10분 안에 또 보내지 않습니다.
|
||||||
|
|
||||||
|
> **생활 속 예 · 냉장고 온도 경보.** 가게 냉장고가 기준 온도를 넘으면 경보가 울려야 합니다. 하지만 1분마다 계속 울리면 직원은 곧 꺼 버립니다. 그래서 "한 번 울리고, 10분 안에는 다시 울리지 않되, 기록에는 남긴다"로 정합니다. 기준 안이면 울리지 않습니다. 과제 2는 SLO를 냉장고 온도처럼 지켜보는 경보를 만드는 실습입니다.
|
||||||
|
|
||||||
|
> "주간 보고는 월요일에나 봐요. SLO를 어기면 바로 알 수 있게 알림을 남겨 주세요. 대신 같은 걸로 계속 울리면 안 됩니다."
|
||||||
|
|
||||||
|
| 조건 | 값 |
|
||||||
|
| --- | --- |
|
||||||
|
| 파일 · 웹훅 | `my-work/slo-alert.json` (새로) · `my-slo-alert` (입력: 보고서와 같음 + `slo`) |
|
||||||
|
| 판정 | `my-log-report`를 불러 `slo.breaches` |
|
||||||
|
| 알림 | 어겼고 10분 안에 같은 알림이 없으면 `POST /alerts` (high · `slo`) → `sent`, 있으면 `suppressed`, 안 어겼으면 `none` ([AGENTS.md](AGENTS.md) 3절) |
|
||||||
|
| 기대 결과 | `cases/lab03-challenge2.json`, `lab03-slo`도 그대로 |
|
||||||
|
|
||||||
|
**먼저 생각해 보기 (2분)**: 76일차 Lab 02 과제 2(알림 폭주 막기)와 무엇이 같나요? 왜 보내지 않은 것(`suppressed`)도 결과에 남길까요?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge2.json -v # 고치기 전 (웹훅이 아직 없음)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이해하기
|
||||||
|
|
||||||
|
**지표 → 판정 → 알림.** 보고서는 "얼마나", SLO는 "지켰나", 알림은 "누가 지금 알아야 하나"입니다. 셋을 이으면 사람이 매주 보고서를 열지 않아도 약속이 깨진 순간 압니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
W(["기간 · slo"]) --> R["Get report<br/>my-log-report"] --> H["Recent alerts<br/>10분 안 slo 알림"] --> D{"Decide"}
|
||||||
|
D -->|breaches 없음| N(["none"])
|
||||||
|
D -->|있음 · 최근 알림 있음| S(["suppressed"])
|
||||||
|
D -->|있음 · 최근 알림 없음| A["POST /alerts<br/>high · slo"] --> T(["sent"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**알림 피로(alert fatigue).** 같은 원인으로 알림이 계속 오면 사람은 알림을 읽지 않게 되고, 정말 새로운 알림도 놓칩니다 (76일차). 그래서 같은 이유(`reason: slo`)의 알림이 10분 안에 있으면 보내지 않고, 대신 결과에 `suppressed`를 남겨 "확인은 했다"는 기록은 지킵니다.
|
||||||
|
|
||||||
|
**메시지에 숫자를.** "SLO 위반"만으로는 무엇을 할지 모릅니다. `SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02`처럼 **어느 지표가 · 얼마로 · 기준이 얼마인데** 넘었는지를 한 줄에.
|
||||||
|
|
||||||
|
**기대 결과.** 고치기 전 0/6 (404, 알림 0개), 고친 뒤 6/6.
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 테스트가 알림을 비우고 시작</summary>
|
||||||
|
|
||||||
|
`cases/lab03-challenge2.json` 맨 위 `before`가 `/alerts-clear`를 부릅니다. 그래서 첫 케이스는 늘 `sent`, 같은 기간을 다시 물은 셋째 케이스는 `suppressed`여야 합니다. 워크플로우 안에서는 알림을 지우지 않습니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대</summary>
|
||||||
|
|
||||||
|
요청문에 꼭 들어갈 것: **① 최근의 길이 ② 보내지 않을 때의 표시 ③ 알림 등급 ④ 결과의 모양**. 아래 빈칸 4개를 채워 `my-work/PROMPTS.md`에 쓴 뒤 에이전트에게 보냅니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/slo-alert.json 을 새로 만들어 줘. 웹훅 POST my-slo-alert, 본문 from · to · tag · source · slo, 이름 "77 SLO alert (my)".
|
||||||
|
Get report: my-log-report 를 부른다 (본문 그대로). Recent alerts: POST http://data-api:8800/alerts/list {workflow: "slo", reason: "slo", within: ____①}.
|
||||||
|
Decide(Code): breaches 가 없으면 alert "none", 있는데 10분 안의 같은 알림이 있으면 "____②", 아니면 "sent".
|
||||||
|
message 는 "SLO breach: error_rate 0.0588 > 0.01, …" 처럼 지표 · 값 · 기준. Send it?(If): sent 일 때만 POST /alerts {workflow: "slo", severity: "____③", reason: "slo", breaches, message}.
|
||||||
|
두 갈래 모두 같은 결과 {ok, breaches, alert, message, total} 로 ____④. 기대 결과 cases/lab03-challenge2.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
빈칸 힌트:
|
||||||
|
|
||||||
|
- ① 10분을 초로.
|
||||||
|
- ② 76일차 Lab 02 과제 2에서 쓴 말과 같습니다.
|
||||||
|
- ③ 76일차 알림 등급: 문의를 잃는 중이면 critical, 처리할 수 있는 문제면? SLO 위반은 어느 쪽인가요?
|
||||||
|
- ④ `If`로 갈래가 나뉘어도, 부른 쪽이 받는 것은 늘 같은 모양이어야 합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/slo-alert.json
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
uv run tools/flow.py ops --reset # 끝나면: 오늘 기록 · 더한 FAQ · 알림 비우기
|
||||||
|
```
|
||||||
|
|
||||||
|
마지막으로 `OPS.md`, [테스트 보고서](templates/TEST-REPORT-TEMPLATE.md)를 `my-work/TEST-REPORT.md`로 채웁니다.
|
||||||
|
|
||||||
|
### 실행 결과 · SLO 알림
|
||||||
|
|
||||||
|
수업 준비 때(10/31) 강사 노트북에서 실제로 나온 출력입니다.
|
||||||
|
|
||||||
|
#### 고치기 전 · SLO 알림 웹훅이 아직 없음
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄만):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✘ 둘째 주 · SLO 못 지킴 → 알림 · 0.0s · status 404 (expected 200); ok = null (expected false); breaches = null (expected ["error_rate", "unanswered_rate"]); alert = null (expected sent); message has none of ['error_rate 0.0588 > 0.01']
|
||||||
|
✘ 알림 확인 (high · slo) · 0.0s · workflow = null (expected slo); severity = null (expected high); reason = null (expected slo); message has none of ['unanswered_rate']
|
||||||
|
✘ 10분 안에 다시 확인 → 보내지 않음 · 0.0s · status 404 (expected 200); ok = null (expected false); alert = null (expected suppressed)
|
||||||
|
✘ slo 알림은 1개 · 0.0s · count = 0 (expected 1)
|
||||||
|
✘ 기준 안이면 알림 없음 · 0.0s · status 404 (expected 200); ok = null (expected true); breaches = null (expected []); alert = null (expected none)
|
||||||
|
✘ 그래도 알림은 1개 · 0.0s · count = 0 (expected 1)
|
||||||
|
Passed 0/6
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 이렇게 나왔나 (고치기 전)**
|
||||||
|
|
||||||
|
- 첫 줄은 테스트가 알림을 비운 것입니다. 그 뒤 `my-slo-alert`가 없어 404, 알림을 읽는 케이스(둘째 · 넷째 · 여섯째)는 데이터 API에 알림이 0개라 ✘입니다 (`latest`는 빈 `{}`, `count` 0).
|
||||||
|
- 둘째 주는 SLO를 어겼는데(예제 2) **아무도 모릅니다.** 주간 보고가 나오는 월요일까지 기다려야 합니다.
|
||||||
|
- 0/6은 **예상한 결과**입니다.
|
||||||
|
|
||||||
|
#### 고친 뒤
|
||||||
|
|
||||||
|
실행한 명령:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/flow.py push my-work/slo-alert.json
|
||||||
|
uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
uv run tools/flow.py test cases/lab03-slo.json
|
||||||
|
uv run tools/flow.py ops
|
||||||
|
uv run tools/flow.py ops --reset
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 (결과 줄과 `ops`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ 둘째 주 · SLO 못 지킴 → 알림 · 1.0s
|
||||||
|
✔ 알림 확인 (high · slo) · 0.0s
|
||||||
|
✔ 10분 안에 다시 확인 → 보내지 않음 · 0.3s
|
||||||
|
✔ slo 알림은 1개 · 0.0s
|
||||||
|
✔ 기준 안이면 알림 없음 · 0.1s
|
||||||
|
✔ 그래도 알림은 1개 · 0.0s
|
||||||
|
Passed 6/6
|
||||||
|
(lab03-slo 4/4)
|
||||||
|
|
||||||
|
alerts: 1
|
||||||
|
[high] slo · slo · - · SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476
|
||||||
|
|
||||||
|
✔ live run logs, added FAQ, created tickets, DLQ and alerts emptied, fault switch all ok
|
||||||
|
```
|
||||||
|
|
||||||
|
**고친 뒤 이렇게 나온 까닭**
|
||||||
|
|
||||||
|
| 순서 | 물은 것 | 결과 | 까닭 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | 둘째 주 | `sent` | 두 지표를 어겼고, 10분 안의 `slo` 알림이 없음 |
|
||||||
|
| 2 | 알림 확인 | high · `slo` · 메시지에 두 지표 | 1에서 남긴 알림 |
|
||||||
|
| 3 | 둘째 주 다시 | **`suppressed`** | 10분 안에 같은 알림(1)이 있음 |
|
||||||
|
| 4 | 알림 수 | 1 | 3은 보내지 않았음 |
|
||||||
|
| 5 | 첫 주 · 기준 0.06 | `none` | 어긴 지표가 없음 |
|
||||||
|
| 6 | 알림 수 | 1 | 5도 보내지 않았음 |
|
||||||
|
|
||||||
|
- **메시지 한 줄로 무엇을 할지 압니다:** `error_rate 0.0588 > 0.01, unanswered_rate 0.0476 …` — 어느 지표가 얼마로, 기준이 얼마인데 넘었는지. `ops`는 한 줄을 60자로 줄여 보여 줘서 뒤가 잘렸습니다 (전체는 `unanswered_rate 0.0476 > 0.02`).
|
||||||
|
- **`suppressed`도 결과입니다.** 같은 문제로 다시 확인한 것은 기록에 남고, 사람에게는 한 번만 갑니다. 10분이 지나도 어긴 상태라면 다시 `sent`가 됩니다.
|
||||||
|
- **1.0초 · 0.3초:** 보고서를 부르고(0.1초) 알림을 읽고 쓰는 것뿐이라 빠릅니다. n8n의 Schedule 트리거로 10분마다 돌려도 부담이 없습니다 (79일차 Ops Assistant로 이어짐).
|
||||||
|
- 마지막 `ops --reset`으로 오늘 기록 30줄 · 더한 FAQ · 알림을 비웠습니다.
|
||||||
|
|
||||||
|
## 막혔을 때
|
||||||
|
|
||||||
|
| 증상 | 할 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| `replay-after`에서 와이파이만 실패 | FAQ를 더했는지 (`curl -s 'localhost:8800/faq?q=와이파이'`, `ops`의 `added FAQ`) |
|
||||||
|
| `replay-after`에서 모두 실패 | timeout을 늘렸는지, push했는지 (`fault faq slow`는 테스트가 켬) |
|
||||||
|
| `lab03` 숫자가 10이 아님 | 실습 기록이 섞임: `replay-before`를 다시 (처음에 `/logs-reset` · `/faq-reset`을 부름) → FAQ 더하기 → `replay-after` |
|
||||||
|
| `lab03-cost`의 `normal`이 0건 | `replay-normal`을 먼저 돌렸는지 |
|
||||||
|
| SLO가 늘 `ok` | 비교가 `>`인지, `limits` 기본값 · 본문 `slo` 덮어쓰기 |
|
||||||
|
| 주간 보고가 timeout | 부르는 웹훅 셋(`my-log-report` · `my-log-summary`)이 모두 push돼 있는지, HTTP timeout 30~60초 |
|
||||||
|
| SLO 알림이 늘 `sent` | Recent alerts의 본문(`workflow` · `reason` · `within: 600`)과, Decide가 그 `count`를 보는지 |
|
||||||
|
| 계속 안 됨 | `reference/lab03-example1-helpdesk-fixed.json`, `reference/lab03-example2-slo.json` |
|
||||||
|
|
||||||
|
## 확인
|
||||||
|
|
||||||
|
- [ ] 예제 1: `replay-before` → 고치기(워크플로우 · FAQ) → `replay-after` 10/10, `cases/lab03.json` 4/4, `OPS.md` 4.1.
|
||||||
|
- [ ] 예제 2: `cases/lab03-slo.json` 4/4, 고친 뒤 남은 breach를 설명했다.
|
||||||
|
- [ ] 예제 3: `replay-normal` 10/10 · `lab03-cost` 4/4, `OPS.md` 4.2 (새로 나빠진 것).
|
||||||
|
- [ ] 과제 1: `cases/lab03-challenge.json` 7/7, 주간 보고 마크다운을 `OPS.md` 6절에.
|
||||||
|
- [ ] 과제 2: `cases/lab03-challenge2.json` 6/6, `lab03-slo`도 통과.
|
||||||
|
- [ ] `TEST-REPORT.md`, 끝나면 `uv run tools/flow.py ops --reset`.
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# 77일차 수강 안내 · 수업 준비부터 마칠 때까지
|
||||||
|
|
||||||
|
- 일자: 2026-11-02 (월) · 77일차
|
||||||
|
- 주제: 실행 로그 분석과 운영 데이터 기반 Workflow 개선 (기록 읽기 · 지표 → 문제 찾기 → 고치고 다시 재기)
|
||||||
|
- 실습 폴더: [Last3Mon/77](README.md) · 강의 자료: [Lecture/77](../../Lecture/77/README.md)
|
||||||
|
- 환경: 65일차 n8n 2.42.6 + **OpenAI API 키만**(`gpt-4.1-mini`, 요약 · 헬프데스크 답) + **77일차 판 데이터 API**(76일차 + 두 주 실행 기록 666줄 · 오늘 실습 기록 · FAQ 더하기) + CLI 코딩 에이전트 하나. **Ollama는 쓰지 않습니다.**
|
||||||
|
- 이 문서: 오늘 하루 내가 할 일을 수업 준비부터 마칠 때까지 순서대로 모은 수강 안내입니다. 수업 중에도 띄워 두고 어디까지 왔는지 확인하세요.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
P["전날<br/>준비 · base-report 게시 ·<br/>기록 훑기"] --> M["아침<br/>켜기 · doctor"] --> L1["1교시 강의 · 2교시<br/>Lab 01 실습 5개<br/>기준 비교 · 지표 · 경계"] --> L2["3교시 강의 · 4교시<br/>Lab 02 실습 5개<br/>묶음 · 이상 · 요약"] --> L3["5교시 강의 · 6교시<br/>Lab 03 실습 5개<br/>고치기 · SLO · 보고 · 알림"] --> V["7~8교시<br/>tlog"] --> E["마친 뒤<br/>ops --reset · 78 준비"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 0. 하루 한눈에
|
||||||
|
|
||||||
|
| 교시 | 시간 | 할 일 | 문서 | 끝났다는 표시 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 전날 | 약 10분 | 자료 받기 · n8n · 77일차 데이터 API · `base-report` 게시 · 기록 훑기 · doctor | [PREP](../../Lecture/77/00-DAY77-PREP.md) | `ready` |
|
||||||
|
| 1 | 09:00~09:50 | 개요 · 1단계 강의 (실행 기록 읽기와 지표) | [개요](../../Lecture/77/00-DAY77-OVERVIEW.md) 1~14장, [1단계](../../Lecture/77/01-STAGE1-READ-THE-LOG.md) | |
|
||||||
|
| 2 | 10:00~10:50 | Lab 01 실습 5개 (예제 1~3 · 과제 1~2) | [Lab 01](LAB-01-Q-READ-AND-MEASURE.md) | `lab01-challenge` 6/6 · `lab01-challenge2` 5/5 |
|
||||||
|
| 3 | 11:00~11:50 | 2단계 강의 (문제를 숫자로 찾기) | 개요 15~21장, [2단계](../../Lecture/77/02-STAGE2-FIND-PROBLEMS.md) | |
|
||||||
|
| 4 | 12:00~12:50 | Lab 02 실습 5개 | [Lab 02](LAB-02-Q-FIND-PROBLEMS.md) | `lab02-challenge` 5/5 · `lab02-challenge2` 3/3 |
|
||||||
|
| - | 13:00~14:00 | 점심 | | |
|
||||||
|
| 5 | 14:00~14:50 | 3단계 강의 (고치고 다시 재기) | 개요 22~28장, [3단계](../../Lecture/77/03-STAGE3-IMPROVE-AND-REMEASURE.md) | |
|
||||||
|
| 6 | 15:00~15:50 | Lab 03 실습 5개 | [Lab 03](LAB-03-Q-IMPROVE-AND-REMEASURE.md) | `lab03-challenge` 7/7 · `lab03-challenge2` 6/6, `OPS.md` |
|
||||||
|
| 7 | 16:00~16:50 | 바이브 코딩 `tlog`: 요청서 → TODO → SPEC → IMP-PLAN → 만들기 → 설치 | [VIBE-REQ-TLOG](VIBE-REQ-TLOG.md) → [VIBE-Q-TLOG](VIBE-Q-TLOG.md) | `Doc/*.md` · R1~R5 · R7 · R11~R14 · `pytest` |
|
||||||
|
| 8 | 17:00~17:50 | 바이브 코딩 발표 · **4단계 정리** (오늘의 의미 · 효과 · 회사에서 쓰는 법) · 78일차 예고 | [4단계 정리](../../Lecture/77/04-STAGE4-WRAPUP.md), [진행](../../Lecture/77/05-VIBE-CODING.md) | 최종 프로젝트 체크리스트 |
|
||||||
|
|
||||||
|
## 1. 전날 준비 (약 10분)
|
||||||
|
|
||||||
|
### 1.1 자료 받기
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/aio-01d4-stu && git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 n8n과 **77일차** 데이터 API 켜기 (Ollama는 끔)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now ollama 2>/dev/null # WSL에 설치한 Ollama가 있으면 끄고 자동 시작도 끔
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/65/n8n
|
||||||
|
docker compose rm -sf ollama # Ollama 컨테이너가 있으면 지움 (없으면 그냥 지나감)
|
||||||
|
docker compose start
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/77/data-api
|
||||||
|
docker compose up -d --force-recreate # 76일차 판을 77일차 판(+ 실행 기록 · FAQ 더하기)으로 바꿈
|
||||||
|
sleep 3 # 데이터 API가 뜰 때까지
|
||||||
|
curl -s 'localhost:8800/logs?limit=1' # {"count": 666, "logs": [ ... ]}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 76일차 연습을 되돌리지 않았어도 괜찮습니다: 데이터 API를 새로 띄우면 접수 건 · DLQ · 알림 · 고장 스위치가 처음으로 돌아갑니다. 두 주 실행 기록 666줄은 늘 그대로입니다.
|
||||||
|
|
||||||
|
### 1.3 기준 보고서 게시 · 준비 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/77
|
||||||
|
uv run tools/flow.py push reference/baseline-report.json
|
||||||
|
uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 3 # 70 run(s) · errors 16
|
||||||
|
uv run tools/flow.py ops --reset
|
||||||
|
uv run tools/flow.py doctor # 마지막 줄 ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 미리 읽기 (선택, 5분)
|
||||||
|
|
||||||
|
[PREP 3절](../../Lecture/77/00-DAY77-PREP.md): "두 주 동안 몇 번 실패했나?"에 옆 사람과 다른 숫자가 나왔다면 무엇이 달랐을까? 그리고 7교시 [요청서](VIBE-REQ-TLOG.md).
|
||||||
|
|
||||||
|
### 준비물
|
||||||
|
|
||||||
|
노트북과 충전기, WSL의 Docker, WSL 창 두 개, 브라우저 (n8n 화면), 숫자를 적을 `my-work/OPS.md`.
|
||||||
|
|
||||||
|
## 2. 수업 당일 아침 (09:00 전)
|
||||||
|
|
||||||
|
1. 노트북을 껐다 켰다면 1.2를 다시 합니다 (오늘 실습 기록 · 더한 FAQ는 처음으로 돌아갑니다 — 괜찮음). `base-report`는 n8n에 그대로 있습니다.
|
||||||
|
2. 터미널 두 개, 둘 다 `cd ~/aio-01d4-stu/Last3Mon/77`. ① 에이전트(`codex`/`claude`/`agy`), ② 확인(`logs` · `call` · `push` · `test` · `ops`).
|
||||||
|
3. 터미널 ②에서 `uv run tools/flow.py doctor` → `ready`, `uv run tools/flow.py ops`.
|
||||||
|
4. `cp templates/OPS-ANALYSIS-TEMPLATE.md my-work/OPS.md`, `my-work/PROMPTS.md`.
|
||||||
|
|
||||||
|
## 3. 실습할 때마다 같은 순서 (오늘은 "재기 → 정의 → 고치기 → 다시 재기")
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["① 먼저 재기<br/>logs · call · test(전)"] --> B["② 정의 · 요청문을<br/>메모장에"] --> C["③ 에이전트에게"] --> D["④ 에이전트가<br/>my-work · check"] --> E["⑤ 내가<br/>push · test -v"]
|
||||||
|
E -->|✘| F["⑥ 실패 줄의<br/>칸 = 실제 (expected 기대)<br/>그대로 붙여 주기"] --> C
|
||||||
|
E -->|✔| G["⑦ OPS.md 한 줄"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 숫자가 기대와 다르면 거의 언제나 **정의**(끝날 포함 · 반올림 · 분모 · 0건)가 다른 것입니다. `✘` 줄 끝의 `칸 = 실제 (expected 기대)`를 그대로 에이전트에게 붙여 줍니다.
|
||||||
|
- 오늘 보고서는 **한 파일**(`my-work/log-report.json`)에 칸을 더해 갑니다. 고칠 때마다 앞의 테스트도 다시 돌립니다 (회귀).
|
||||||
|
- 두 주 기록은 늘 같으므로 지표 숫자도 누구에게나 같습니다. 오늘 실습 기록(`source: live`)만 시간이 실행마다 다릅니다.
|
||||||
|
|
||||||
|
## 4. 1~2교시 · 1단계 · 실행 기록 읽기와 지표
|
||||||
|
|
||||||
|
1교시는 강의, 2교시는 [Lab 01](LAB-01-Q-READ-AND-MEASURE.md) (50분, 실습 5개).
|
||||||
|
|
||||||
|
| 순서 | 할 일 | 파일 · 웹훅 | 확인 (터미널 ②) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 예제 1 (10분, 에이전트 없이) | 두 주 · 하루를 `logs`로 훑고, 같은 기간을 팀의 기준 보고서에 | `base-report` | `logs --from … --to …`, `call base-report requests/two-weeks.json` · `monday.json` → `OPS.md` 1.1 |
|
||||||
|
| 예제 2 (15분) | 정의대로 세는 내 보고서: 실행 · 오류율 · p50 · p95 · 갈래 | `my-work/log-report.json` (새로) · `my-log-report` | `test cases/lab01.json --webhook base-report -v` (전), `push` → `test cases/lab01.json -v` |
|
||||||
|
| 예제 3 (7분, 에이전트 없이) | 경계에서 재 보기: 하루 · 빈 기간 · 주말 · `source` | (그대로) | `test cases/lab01-edges.json -v`, `--webhook base-report` → `OPS.md` 1.2 |
|
||||||
|
| 과제 1 (8분) | 날짜별 · 릴리스별 다시 묻는 비율 | `my-work/log-report.json` 고치기 | `test cases/lab01-challenge.json -v`, `lab01` |
|
||||||
|
| 과제 2 (10분) | 단계별 p50 · p95, 비용 · 실행당 비용 · 호출 수 | `my-work/log-report.json` 고치기 | `test cases/lab01-challenge2.json -v`, `lab01` · `lab01-challenge` |
|
||||||
|
|
||||||
|
(명령 앞에는 모두 `uv run tools/flow.py`가 붙습니다.)
|
||||||
|
|
||||||
|
## 5. 3~4교시 · 2단계 · 문제를 숫자로 찾기
|
||||||
|
|
||||||
|
3교시는 강의, 4교시는 [Lab 02](LAB-02-Q-FIND-PROBLEMS.md) (50분, 실습 5개).
|
||||||
|
|
||||||
|
| 순서 | 할 일 | 확인 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 예제 1 (12분) | 실패를 (노드, 메시지)로 묶기 · 답 못 한 주제 세기 | `test cases/lab02.json -v` (전 · 후: 0/6 → 3/6), `curl -s 'localhost:8800/faq?q=와이파이'` → `OPS.md` 3.1 |
|
||||||
|
| 예제 2 (10분) | 이상 시간대: 5건 이상 · 전체 오류율의 3배 · 오류 1건 이상 | `test cases/lab02.json -v` 6/6, `lab01-challenge2`, `call my-log-report requests/two-weeks.json` |
|
||||||
|
| 예제 3 (6분, 에이전트 없이) | 기간을 바꾸면 문턱 · 이상이 바뀐다 | `test cases/lab02-periods.json -v` → `OPS.md` 3.2 |
|
||||||
|
| 과제 1 (10분) | 답했는데 다시 묻는 주제 (수와 비율) | `test cases/lab02-challenge.json -v`, `lab02` |
|
||||||
|
| 과제 2 (12분) | LLM 세 문장 요약, 숫자는 Code가 확인 | `my-work/log-summary.json` (새로), `test cases/lab02-challenge2.json -v`, `call my-log-summary requests/week2.json` |
|
||||||
|
|
||||||
|
## 6. 5~6교시 · 3단계 · 고치고 다시 재기
|
||||||
|
|
||||||
|
5교시는 강의, 6교시는 [Lab 03](LAB-03-Q-IMPROVE-AND-REMEASURE.md) (50분, 실습 5개).
|
||||||
|
|
||||||
|
| 순서 | 할 일 | 확인 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 예제 1 (14분) | 제공 헬프데스크로 고치기 전(`before`) → timeout · 재시도(에이전트) + 와이파이 FAQ(나) → 고친 뒤(`after`) | `cp reference/helpdesk-live.json my-work/helpdesk.json`, `test cases/replay-before.json -v`, `curl … /faq/add`, `test cases/replay-after.json -v`, `test cases/lab03.json -v` |
|
||||||
|
| 예제 2 (8분) | SLO 판정: `unanswered_rate` · `slo.breaches` | `test cases/lab03-slo.json -v`, `call my-log-report '{"tag": "after", "source": "live"}'` |
|
||||||
|
| 예제 3 (6분, 에이전트 없이) | 고친 것의 대가: 평소 FAQ로 같은 10개(`normal`), 단계 시간으로 비교 | `test cases/replay-normal.json -v`, `test cases/lab03-cost.json -v`, `ops` → `OPS.md` 4.2 |
|
||||||
|
| 과제 1 (12분) | 주간 보고: 보고서 둘 + 요약을 다시 불러 마크다운 | `my-work/weekly.json` (새로), `test cases/lab03-challenge.json -v`, `call my-weekly requests/weekly.json` |
|
||||||
|
| 과제 2 (10분) | SLO를 어기면 high 알림, 10분 안 같은 알림은 `suppressed` | `my-work/slo-alert.json` (새로), `test cases/lab03-challenge2.json -v`, `lab03-slo`, `ops --reset` |
|
||||||
|
|
||||||
|
- 기록: `OPS.md` 전체, `TEST-REPORT.md`.
|
||||||
|
|
||||||
|
## 7. 7~8교시 · 바이브 코딩 `tlog`
|
||||||
|
|
||||||
|
[VIBE-REQ-TLOG](VIBE-REQ-TLOG.md)(요청서)를 **먼저** 읽고 [VIBE-Q-TLOG](VIBE-Q-TLOG.md)의 순서대로 갑니다: `Doc/TODO.md` → `Doc/SPEC.md` → `Doc/IMP-PLAN.md`(엔진 · TUI 단계) → 단계별 구현 → 확인 → `uv tool install` → 다른 폴더에서 사용. 요구는 14개(R1~R14)이고 50분 안에는 엔진 1 · 2단계 · TUI 1단계 · 설치까지, 나머지(R6 단계 · 비용, R8 기간 비교, R9 다른 기록 읽기, R10 주간 보고)는 `Doc/IMP-PLAN.md`의 다음 단계로 집에서 이어 갑니다. 만드는 것은 실행 기록(파일 또는 데이터 API)을 오늘 Lab의 정의 그대로 세어 **같은 숫자**를 내고, 실패 묶음 · 이상 시간대 · SLO 판정(종료 코드)을 보여 주고, TUI로 묶음을 골라 그 실행들을 보는 도구. 정답본 `VIBE-A-TLOG.md`는 수업이 끝나면 `git pull`로 받습니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/vibe/tlog/Doc && cd ~/vibe/tlog
|
||||||
|
git init
|
||||||
|
# 에이전트는 Doc/TODO.md를 쓴 뒤에 켭니다: codex (또는 claude, agy)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 시간 | 단계 | 확인 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 16:00~16:09 | 0 준비 · 1 요청서 → `Doc/TODO.md` (에이전트 없이) | 할 일 ①~⑫ · 꼭 / 나중 |
|
||||||
|
| 16:09~16:15 | 2 `Doc/SPEC.md` → VIBE-Q 요구 표(R1~R14)와 비교 · commit | 요구마다 확인 방법 (두 주 666 · 21 · 0.0315 · p95 2537, 종료 코드) |
|
||||||
|
| 16:15~16:20 | 3 `Doc/IMP-PLAN.md` (엔진 1~3단계 · TUI 1단계 · 설치) · commit | 단계마다 체크리스트 |
|
||||||
|
| 16:20~16:44 | 4 구현: 엔진 1단계(R1 · R2 · R5 · R12 · R13) → 엔진 2단계(R3 · R4 · R7) → TUI 1단계(R11), 막히면 5 문제 해결 팁 | 단계마다 `pytest` · 직접 실행 · commit |
|
||||||
|
| 16:44~16:50 | 6 확인 · 7 `uv tool install --editable .` · 8 다른 폴더에서 `tlog` | `tlog summary --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30` = `my-log-report`의 숫자 |
|
||||||
|
|
||||||
|
- 8교시: 1~2명 발표 (10분) → [4단계 정리](../../Lecture/77/04-STAGE4-WRAPUP.md) (35분: 오늘 한 일의 의미 · 효과 · 산업에서의 응용 · 면접 질문 · 최종 프로젝트 체크리스트) → 78일차 예고.
|
||||||
|
|
||||||
|
## 8. 마친 뒤
|
||||||
|
|
||||||
|
| 할 일 | 이유 |
|
||||||
|
| --- | --- |
|
||||||
|
| `uv run tools/flow.py ops --reset`, `doctor` → `ready` | 오늘 실습 기록 · 더한 FAQ · 알림을 비워 내일 처음 상태로 |
|
||||||
|
| `PROMPTS.md`, `OPS.md`, `TEST-REPORT.md` 정리 | 최종 프로젝트 재료 (운영 지표 · 주간 보고) |
|
||||||
|
| `my-work/*.json`, `~/vibe/tlog` 지우지 않기 | 78일차 품질 검증 · 79일차 Ops Assistant(`tops`) |
|
||||||
|
| 78일차 준비: [00-DAY78-PREP](../../Lecture/78/00-DAY78-PREP.md) | 데이터 품질 검증 |
|
||||||
|
|
||||||
|
## 9. 막혔을 때 (오늘 공통)
|
||||||
|
|
||||||
|
| 증상 | 할 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| doctor의 `run logs /logs (666 seeded runs)` ✘ | 데이터 API가 예전 판. 1.2의 77일차 `--force-recreate` · `sleep 3` |
|
||||||
|
| doctor에 `live run log(s) left` · `FAQ entr(ies) added` | `uv run tools/flow.py ops --reset` |
|
||||||
|
| `total`이 666 · 357이 아님 | `from` · `to`를 그대로 넘기는지 (직접 거르지 않음), `source` 기본값 `all` |
|
||||||
|
| `p95_ms`가 1~2 다름 | 오름차순 · `ceil(p × n)`번째 (1부터), 오류 실행 포함 |
|
||||||
|
| 비율이 0.03149… · 3.15 | 비율, 소수 4자리 반올림 |
|
||||||
|
| 앞에서 통과한 테스트가 깨짐 | 같은 파일에 칸을 더할 때 앞의 지표를 지웠는지 — `uv run tools/flow.py diff reference/<앞 단계>.json my-work/log-report.json` |
|
||||||
|
| `lab03` 숫자가 10이 아님 | `replay-before`부터 다시 (처음에 오늘 기록 · FAQ를 비움) |
|
||||||
|
| `call my-…`가 404 | push했는지, 웹훅 경로 |
|
||||||
|
| 같은 곳을 3번 고쳐도 실패 | 요청을 더 작게, 또는 `reference/`로 이어 가기 |
|
||||||
|
|
||||||
|
## 확인
|
||||||
|
|
||||||
|
- [ ] 전날 77일차 데이터 API, `base-report` 게시, `doctor` → `ready`
|
||||||
|
- [ ] Lab 01: `OPS.md` 1.1 · 1.2, `lab01` 5/5, `lab01-edges` 6/6, `lab01-challenge` 6/6, `lab01-challenge2` 5/5
|
||||||
|
- [ ] Lab 02: `lab02` 6/6, `lab02-periods` 5/5, `lab02-challenge` 5/5, `lab02-challenge2` 3/3, `OPS.md` 3절
|
||||||
|
- [ ] Lab 03: `replay-after` 10/10 · `lab03` 4/4, `lab03-slo` 4/4, `lab03-cost` 4/4, `lab03-challenge` 7/7, `lab03-challenge2` 6/6, `OPS.md` 4~6절
|
||||||
|
- [ ] `tlog`: `Doc/TODO.md` · `SPEC.md` · `IMP-PLAN.md` 먼저, 엔진 1 · 2단계 · TUI 1단계, `pytest`, 단계마다 commit, `uv tool install`
|
||||||
|
- [ ] `ops --reset`, 78일차 준비
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 77일차 실습 · 실행 로그 분석과 운영 데이터 기반 Workflow 개선 (바이브 코딩)
|
||||||
|
|
||||||
|
- 일자: 2026-11-02 (월, 77일차)
|
||||||
|
- 교과목: 노코드 멀티 에이전트 워크플로우 설계 및 운영
|
||||||
|
- 단원: AI 워크플로우 운영 및 관리 역량 강화
|
||||||
|
- 강의 자료: [Lecture/77](../../Lecture/77/README.md)
|
||||||
|
- 하루 수강 안내: [LECTURE-77.md](LECTURE-77.md) (수업 준비부터 마칠 때까지 순서대로)
|
||||||
|
- 67~76일차처럼 CLI 코딩 에이전트에게 **내가 쓴 요청문**으로 워크플로우를 만들게 하고, **테스트로 확인**합니다. 오늘은 **두 주 동안의 실행 기록**을 정의대로 세어 지표로 만들고, 문제를 숫자로 찾고, 고친 뒤 같은 질문으로 다시 잽니다.
|
||||||
|
|
||||||
|
## 오늘 보는 것: 헬프데스크 두 주(10/19~10/30)의 실행 기록 666줄
|
||||||
|
|
||||||
|
데이터 API에 헬프데스크의 실행 기록이 쌓여 있습니다. 그 안에 운영팀이 아직 모르는 문제가 몇 개 숨어 있고, 팀이 쓰던 보고서(`base-report`)는 다른 정의로 세고 있었습니다. 감으로 말하지 않고 **정의된 지표로** 찾습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
L[("실행 기록<br/>/logs")] --> R["로그 보고서<br/>my-log-report<br/>지표 · 날짜 · 릴리스 · 단계 · 비용"]
|
||||||
|
R --> P["문제 찾기<br/>실패 묶음 · 답 못 한 주제 ·<br/>이상 시간대 · 다시 묻는 주제"]
|
||||||
|
P --> S{{"요약 (LLM)<br/>숫자는 Code가 확인"}}
|
||||||
|
P --> F["고치기<br/>timeout · FAQ 더하기"] --> RR["다시 돌려 보기<br/>before · after · normal"] --> R
|
||||||
|
R --> W["SLO · 주간 보고 · SLO 알림"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 일하는 방식 (67~76일차와 같음)
|
||||||
|
|
||||||
|
| 누가 | 무엇을 |
|
||||||
|
| --- | --- |
|
||||||
|
| 나 | 기록을 읽고 질문하기, 지표 정의 정하기, 기준과 비교, 문제의 순서 정하기, 콘텐츠(FAQ) 고치기, 고친 뒤 다시 재기 |
|
||||||
|
| CLI 에이전트 | [AGENTS.md](AGENTS.md) 규칙대로 워크플로우 JSON 작성, `check`로 형식 오류 고치기 |
|
||||||
|
| `tools/flow.py` | check · push · call · test(`--webhook`, `range`) · diff · **logs**(기록 훑어보기) · **ops**(오늘 기록 · 더한 FAQ · 알림, `--reset`) · fault · doctor (모두 `-v`) |
|
||||||
|
|
||||||
|
> **숫자는 Code가, 글은 LLM이.** LLM에게 666줄을 주고 "문제가 뭐야?"라고 묻지 않습니다. 그리고 숫자가 다르면 먼저 **정의**를 의심합니다.
|
||||||
|
|
||||||
|
## 실습 15개 (Lab마다 예제 3 · 과제 2)
|
||||||
|
|
||||||
|
| | 예제 1 | 예제 2 | 예제 3 | 과제 1 | 과제 2 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| [Lab 01](LAB-01-Q-READ-AND-MEASURE.md) | 손으로 읽기 · 기준 보고서 (에이전트 없이) | 지표 보고서 | 경계에서 재 보기 (에이전트 없이) | 날짜별 · 릴리스별 | 단계별 · 비용 |
|
||||||
|
| [Lab 02](LAB-02-Q-FIND-PROBLEMS.md) | 실패 묶음 · 답 못 한 주제 | 이상 시간대 | 기간을 바꾸면 (에이전트 없이) | 답했는데 다시 묻는 주제 | LLM 요약 · 숫자 확인 |
|
||||||
|
| [Lab 03](LAB-03-Q-IMPROVE-AND-REMEASURE.md) | 고치기 전 → 고치기 → 고친 뒤 | SLO 판정 | 고친 것의 대가 (에이전트 없이) | 주간 보고 | SLO 알림 |
|
||||||
|
|
||||||
|
## 실습 문서 읽는 법
|
||||||
|
|
||||||
|
- 실습 문서(`LAB-0x-Q-*.md`, `VIBE-Q-TLOG.md`)에는 **복사해 붙일 요청문이 없습니다.** 실습마다 `### 이 실습의 의도`(한 줄 · 생활 속 예 · 상황 · 알게 되는 것 · 적을 곳)와 `### 이해하기`(왜 · 지금 → 바뀐 뒤 · 테스트 케이스 · 기대 결과)를 읽고, 막히면 `▶` 를 하나씩 펼칩니다. 요청문은 `▶ 빈칸 채우기`의 빈칸 4개(`____①`~`____④`)를 채워서 씁니다. 실습마다 끝에 `### 실행 결과`(실행한 명령, 고치기 전 · 후 출력과 그렇게 나온 까닭)가 있습니다.
|
||||||
|
- 쓴 요청문은 `my-work/PROMPTS.md`에, 숫자와 문제 목록은 `my-work/OPS.md`에, 테스트 결과는 `my-work/TEST-REPORT.md`에 모아 둡니다.
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
|
||||||
|
```text
|
||||||
|
Last3Mon/77/
|
||||||
|
├── README.md
|
||||||
|
├── LECTURE-77.md # 하루 수강 안내
|
||||||
|
├── AGENTS.md # CLI 에이전트용 작업 규칙 (계약 · 지표 정의 3절)
|
||||||
|
├── LAB-01-Q-READ-AND-MEASURE.md # 1단계 · 기록 읽기 · 기준 비교 · 지표 보고서 · 경계 · 날짜별 · 릴리스별 · 단계 · 비용
|
||||||
|
├── LAB-02-Q-FIND-PROBLEMS.md # 2단계 · 실패 묶음 · 답 못 한 주제 · 이상 시간대 · 기간 · 다시 묻는 주제 · LLM 요약
|
||||||
|
├── LAB-03-Q-IMPROVE-AND-REMEASURE.md # 3단계 · 고치고 다시 돌리기 · SLO · 고친 것의 대가 · 주간 보고 · SLO 알림
|
||||||
|
├── VIBE-REQ-TLOG.md # 7교시 바이브 코딩 요청서 (가장 먼저 읽기)
|
||||||
|
├── VIBE-Q-TLOG.md # 7교시 바이브 코딩 · tlog (실행 기록 분석 · SLO · TUI)
|
||||||
|
├── tools/flow.py
|
||||||
|
├── data-api/ # 77일차 판 데이터 API (76일차 + 실행 기록 · FAQ 더하기)
|
||||||
|
│ └── data/run-logs.json # 두 주 실행 기록 666줄
|
||||||
|
├── cases/ # lab01 · lab01-edges · lab01-challenge(2) · lab02 · lab02-periods · lab02-challenge(2) ·
|
||||||
|
│ # replay-before · replay-after · replay-normal · lab03 · lab03-slo · lab03-cost · lab03-challenge(2)
|
||||||
|
├── requests/ # 기간(two-weeks · week1 · week2 · monday) · weekly · faq-wifi(더할 FAQ)
|
||||||
|
├── reference/ # baseline-report(팀의 기준) · helpdesk-live(제공) + 막혔을 때 이어 가는 워크플로우
|
||||||
|
├── templates/ # 운영 분석표 · 테스트 보고서
|
||||||
|
├── my-work/
|
||||||
|
└── runs/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 준비
|
||||||
|
|
||||||
|
[실습 준비](../../Lecture/77/00-DAY77-PREP.md)를 끝내 주세요. 요약:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/65/n8n && docker compose start # Ollama는 쓰지 않음 · 처음 한 번 PREP 1절대로 끄기
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/77/data-api && docker compose up -d --force-recreate && sleep 3 # 77일차 판 (실행 기록)
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/77 && uv run tools/flow.py push reference/baseline-report.json
|
||||||
|
uv run tools/flow.py doctor # 마지막 줄 ready
|
||||||
|
```
|
||||||
|
|
||||||
|
## `flow.py` 명령 (오늘 더해진 것)
|
||||||
|
|
||||||
|
| 명령 | 하는 일 | 수업 준비 때 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 10` | 기간의 실행 수 · 오류 수 · 갈래, 마지막 N줄 (`-v`면 마지막 줄의 모든 칸) | 70 run(s) · errors 16 (22.9%) |
|
||||||
|
| `uv run tools/flow.py logs --tag after --source live` | 오늘 실습에서 남긴 기록만 | |
|
||||||
|
| `uv run tools/flow.py call base-report requests/two-weeks.json` | 팀의 기준 보고서 | 602건 (끝날이 빠짐) |
|
||||||
|
| `uv run tools/flow.py test cases/lab01.json --webhook base-report` | 같은 케이스를 다른 웹훅에 | 0/5 |
|
||||||
|
| `uv run tools/flow.py ops` · `ops --reset` | 오늘 기록(`tag`마다) · 더한 FAQ · 알림 · 고장 스위치 · 모두 비우기 | |
|
||||||
|
| `uv run tools/flow.py fault faq slow --seconds 1.2` · `fault all ok` | FAQ를 느리게 (테스트가 켜고 끔) | |
|
||||||
|
|
||||||
|
> 오늘 LLM은 Lab 02 과제 2의 요약과 Lab 03 헬프데스크의 답에만 씁니다 (`gpt-4.1-mini`). 지표 · 묶음 · 이상 · SLO는 모두 Code가 0.1초 안팎에 셉니다.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 7교시 바이브 코딩 · `tlog` 정답본
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업 뒤에 올립니다. 학생용: [VIBE-REQ-TLOG.md](VIBE-REQ-TLOG.md) → [VIBE-Q-TLOG.md](VIBE-Q-TLOG.md).
|
||||||
|
> 강사가 직접 만든 본: [tlog/](tlog/README.md) (테스트 23개, `uv tool install --editable .`). 아래 숫자는 2026-10-31 강사 Mac에서 77일차 데이터 API와 n8n `my-log-report`(Lab 03 예제 2 정답)에 실제로 돌린 값입니다. 68일차부터 바이브 코딩은 CLI 에이전트로 다시 만들어 재지 않습니다 (요청문은 예시).
|
||||||
|
|
||||||
|
## 1. TODO 예 (요청의 말 → 할 일)
|
||||||
|
|
||||||
|
| # | 요청의 말 | 할 일 (내 말) | 꼭 · 나중 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| ① | 파일 · API, 기간 · 태그, 끝날 포함 | `--file` · `--url`, `--from` · `--to`(포함) · `--tag` · `--source` | 꼭 |
|
||||||
|
| ② | n8n과 똑같은 숫자 | AGENTS.md 3절 정의 그대로, 반올림까지 JavaScript와 같게, 0건이면 0 | 꼭 |
|
||||||
|
| ③ | 원인으로 묶기 · 넘긴 주제 · 다시 묻는 주제 | `problems`: failures · unanswered_topics · repeat_topics | 꼭 |
|
||||||
|
| ④ | 평소보다 나빴던 시간 | 5건 · 3배 · 1건 이상, 시간순 | 꼭 |
|
||||||
|
| ⑤ | JSON, 같은 칸 이름 | `--json` = `my-log-report` 응답과 같은 칸 | 꼭 |
|
||||||
|
| ⑥ | 단계 · 비용 | `steps`(값 > 0만) · `cost_usd` · `cost_per_run` · `llm_calls` | 나중 |
|
||||||
|
| ⑦ | SLO · 종료 코드 | `slo`, 기준 바꾸기, 위반이면 4 | 꼭 |
|
||||||
|
| ⑧ | 지난주와 나란히 | `compare` | 나중 |
|
||||||
|
| ⑨ | tretry 기록 | 시도 줄 → 실행 한 줄 | 나중 |
|
||||||
|
| ⑩ | 주간 보고 마크다운 | `weekly` (LLM 없음) | 나중 |
|
||||||
|
| ⑪ | 화면 | `ui`: 요약 · 묶음 · 그 실행들 · `[t]` | 꼭 |
|
||||||
|
| ⑫ | 종료 코드 | 0 · 1 · 2 · 4 | 꼭 |
|
||||||
|
| 끝 | n8n 없이 시험 · 같은 숫자 확인 | 두 주 사본 · 가짜 데이터 API | 꼭 |
|
||||||
|
|
||||||
|
## 2. 요청문 (처음 한 번 + 다음 단계 한 줄)
|
||||||
|
|
||||||
|
단계마다 긴 요청문을 쓰지 않습니다. 할 일은 이미 `Doc/SPEC.md` · `Doc/IMP-PLAN.md`에 있으니, 에이전트가 그 파일을 읽게 하면 됩니다.
|
||||||
|
|
||||||
|
### 처음 한 번
|
||||||
|
|
||||||
|
```text
|
||||||
|
Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 체크 안 된 첫 단계만 만들어 줘. uv 프로젝트, 명령 이름 tlog, 코드와 화면 글자는 영어.
|
||||||
|
끝나면 IMP-PLAN 체크를 표시하고 확인 명령을 알려 줘. 다음 단계는 내가 말할 때까지 하지 마.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 그다음 단계마다 (같은 한 줄)
|
||||||
|
|
||||||
|
```text
|
||||||
|
확인했고 commit 했어. 다음 단계.
|
||||||
|
```
|
||||||
|
|
||||||
|
- 그냥 "계속"만 치면 단계 두세 개를 한꺼번에 하거나 체크 · 확인 명령을 빼먹기 쉽습니다. 이 한 줄은 처음 요청의 약속(한 단계만 · 체크 · 확인 명령 · 멈춤)을 이어 가게 합니다.
|
||||||
|
- 대화가 길어지면 새 세션에서 **처음 요청**을 다시 보냅니다. "체크 안 된 첫 단계"부터 이어집니다.
|
||||||
|
- 에이전트가 단계 내용을 잘못 알았다면, 요청문을 길게 쓰기보다 **SPEC · IMP-PLAN을 고치고** 다시 시킵니다.
|
||||||
|
|
||||||
|
### 틀렸을 때만 · 고쳐 달라는 요청문 예
|
||||||
|
|
||||||
|
무엇이 틀렸나(보인 것) · SPEC의 어느 요구인가 · 어디만 고치나 · 무엇으로 확인하나, 넷을 적습니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
tlog summary 가 두 주(10/19~10/30)를 602 runs 라고 해. n8n my-log-report 는 666 이야. SPEC R1 대로 끝날도 넣어야 해 (from <= started_at[:10] <= to).
|
||||||
|
load.py 의 날짜 거르기만 고치고, 10/30 하루만 물으면 64 runs, 두 주 666 인 테스트를 넣어 줘. 다른 파일은 건드리지 마.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 확인 기준과 강사 실측 (10/31, 77일차 데이터 API)
|
||||||
|
|
||||||
|
| 요구 | 기준 | 실측 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| R1 | 파일 · API, 날짜 포함 | API 10/26 하루 → 70 · 16, `--source live` → 오늘 기록만 (가짜 서버 테스트) |
|
||||||
|
| R2 | 두 주 | `666 runs · errors 21 (3.15%) · p50 1205ms · p95 2537ms`, 갈래 591 · 34 · 20 · 21 |
|
||||||
|
| R2 | 빈 기간 11/02~11/06 | 0 · 0 · 0 (n8n과 같음) |
|
||||||
|
| R3 · R4 | `tlog problems` 두 주 | 16 Search FAQ timeout · 5 Answer 429 · 와이파이 34 · 09시 16/16 · 15시 5/7 · 프린터 9.88% (8/81) |
|
||||||
|
| R5 | 같은 기간 `my-log-report`와 칸 비교 | 두 주 · 둘째 주 · 10/26 · 빈 기간 네 번 모두 **18칸 중 다른 칸 0개** |
|
||||||
|
| R6 | 단계 · 비용 | FAQ p95 39 (646) · 답 p95 2509 (596) · 0.07836달러 · 0.000118 · 596 |
|
||||||
|
| R7 | `tlog slo` 둘째 주 | `SLO breach: error_rate, unanswered_rate` · **종료 코드 4**, 첫 주 `--unanswered 0.06` → `SLO ok` · 0 |
|
||||||
|
| R8 | `tlog compare` 첫 주 → 둘째 주 | total 309 → 357 (+48), error_rate 0 → 0.0588, p50 1252 → 1171 (−81), p95 2525 → 2598 (+73) |
|
||||||
|
| R9 | tretry 기록 (scratch 파일, 시도 7줄) | `3 runs · errors 1 (33.33%)`, 갈래 오류 1 · 성공 1 · 대체 1, 실패 묶음 `command · exit 1` |
|
||||||
|
| R10 | `tlog weekly` | 숫자 표가 n8n `my-weekly`와 같음 (`\| 요청 \| 309 \| 357 \|` · 0% → 5.88% · SLO 못 지킴 둘) |
|
||||||
|
| R11 | `run_test` | 요약 줄 `666 runs · errors 21`, 묶음 2줄(16 · 5), 오른쪽 16 → 5줄, `t` → `2026-10-26T09` |
|
||||||
|
| R12 | 인자 없음 · 출처 없음 · 없는 파일 | 2 · 2 · 1 |
|
||||||
|
| R13 | `uv run pytest -q` | 23 passed in 1.7s |
|
||||||
|
| R14 | `/tmp`에서 `tlog --help` | `usage: tlog [-h] {summary,problems,slo,compare,weekly,ui} ...`, `tlog summary --url …` 0.08초 |
|
||||||
|
|
||||||
|
- 수업 준비 때 찾은 실수 둘 (학생도 겪기 쉬움): ① Python `round`는 짝수 쪽으로 반올림해 n8n(JavaScript `Math.round`)과 반에 걸린 값이 달라질 수 있어 `js_round`로 맞춤. ② `$TLOG_URL`을 정해 둔 채 `--file`을 주면 API를 읽던 것 → 파일이 먼저.
|
||||||
|
|
||||||
|
## 4. 7교시에 볼 것
|
||||||
|
|
||||||
|
| 볼 것 | 막혔을 때 한마디 |
|
||||||
|
| --- | --- |
|
||||||
|
| 숫자가 n8n과 다름 | "어느 칸이 · 얼마로 다른지 붙여 주고, AGENTS.md 3절 정의 그대로" |
|
||||||
|
| 빈 기간에 오류 | "0건이면 0" |
|
||||||
|
| 파일 · API의 숫자가 다름 | "거르기 규칙을 데이터 API와 같게 (끝날 포함)" |
|
||||||
|
| 테스트가 진짜 API를 부름 | "가짜 서버로, 진짜는 확인 단계에서만" |
|
||||||
|
| 세는 코드가 여기저기 | "metrics.py 한 곳" |
|
||||||
|
| `slo`가 늘 0 | "크면 위반, 위반이면 4" |
|
||||||
|
| tretry 시도를 실행으로 셈 | "final 줄까지 묶기" |
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
# 7교시 바이브 코딩 · `tlog` 만들기
|
||||||
|
|
||||||
|
> **먼저 [VIBE-REQ-TLOG.md](VIBE-REQ-TLOG.md)(요청서)를 읽고 `~/vibe/tlog/Doc/TODO.md`를 직접 쓴 뒤** 이 문서를 엽니다. 아래 1~8을 차례로 갑니다. 이 문서의 요구 표는 **내 SPEC을 쓴 뒤에** 비교하는 데 쓰고, 에이전트에게 통째로 붙여 넣지 않습니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
T["1. Doc/TODO.md<br/>할 일 정리"] --> S["2. Doc/SPEC.md<br/>확인할 수 있는 요구"] --> P["3. Doc/IMP-PLAN.md<br/>엔진 → TUI 단계"] --> I["4. 구현<br/>단계마다 요청 · 확인"]
|
||||||
|
I --> H["5. 막히면<br/>문제 해결 팁"] --> I
|
||||||
|
I --> C["6. 결과 확인<br/>요구 표 체크"] --> U["7. uv tool install"] --> O["8. 다른 곳에서 사용"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 시간: 16:00~16:50 (50분) · 발표와 정리는 8교시 · 50분 안에 못 한 단계는 집에서 `Doc/IMP-PLAN.md`의 다음 단계로 이어 갑니다
|
||||||
|
- 도구: CLI 코딩 에이전트(codex / claude / agy), Python + uv + [Textual](https://textual.textualize.io/), 표준 라이브러리 `json` · `urllib.request`
|
||||||
|
- 결과물: 실행 기록을 **파일에서도 데이터 API에서도** 읽어, 오늘 Lab의 정의(AGENTS.md 3절) 그대로 세어 **n8n `my-log-report`와 같은 숫자**를 내고, 실패 묶음 · 답 못 한 주제 · 이상 시간대 · 다시 묻는 주제를 보여 주고, **SLO를 판정해 종료 코드로** 알리고, 지난 기간과 비교하고, 주간 보고를 마크다운으로 뽑고, TUI로 묶음을 골라 그 실행들을 보는 도구. 76일차 `tretry` 기록도 같은 모양으로 읽습니다. `uv tool install`로 설치해 **어느 폴더에서나** `tlog`. 오늘 n8n 워크플로우로 한 일을 **터미널 어디서나.**
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ tlog summary --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30
|
||||||
|
357 runs · errors 21 (5.88%) · p50 1171ms · p95 2598ms
|
||||||
|
route: 자동 답변 299 · 오류 21 · 잠시 후 20 · 담당자 연결 17
|
||||||
|
2026-10-26 70 runs errors 16 22.86%
|
||||||
|
…
|
||||||
|
$ tlog slo --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30; echo $?
|
||||||
|
✘ error_rate 0.0588 (limit 0.01)
|
||||||
|
✔ p95_ms 2598 (limit 3000)
|
||||||
|
✘ unanswered_rate 0.0476 (limit 0.02)
|
||||||
|
SLO breach: error_rate, unanswered_rate
|
||||||
|
4
|
||||||
|
|
||||||
|
$ tlog ui --url http://localhost:8800 --from 2026-10-19 --to 2026-10-30
|
||||||
|
┌ tlog · http://localhost:8800 · 2026-10-19 ~ 2026-10-30 ─────────────────────────────────┐
|
||||||
|
│ 666 runs · errors 21 (3.15%) · p50 1205ms · p95 2537ms · unanswered 5.11% · SLO breach … │
|
||||||
|
│ count node message │ started_at status route ms topic │
|
||||||
|
│ 16 Search FAQ timeout of 1000ms ex… │ 2026-10-26T09:05:39 error 오류 1060 비밀번호 │
|
||||||
|
│ 5 Answer 429 Rate limit reac… │ … │
|
||||||
|
│ [t] failures / hours / slow · [r] reload · [q] quit │
|
||||||
|
└───────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
> 요청문은 **직접** 씁니다. 단계마다 `▶` 를 펼치면 생각할 거리가 나옵니다. 정답 요청문은 없습니다.
|
||||||
|
>
|
||||||
|
> **테스트에서 진짜 n8n · 데이터 API를 부르지 않습니다.** 두 주 기록의 사본과, 테스트 안의 작은 가짜 데이터 API로 시험합니다.
|
||||||
|
|
||||||
|
## 시간 (50분)
|
||||||
|
|
||||||
|
| 시간 | 단계 | 결과 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 16:00~16:09 | 0 준비 · 1 `Doc/TODO.md` (요청서 읽고 직접) | 할 일 ①~⑫ · 꼭 / 나중 |
|
||||||
|
| 16:09~16:15 | 2 `Doc/SPEC.md` → 아래 요구 표와 비교 · 고치기 | 요구마다 확인 방법 |
|
||||||
|
| 16:15~16:20 | 3 `Doc/IMP-PLAN.md` (엔진 단계 · TUI 단계) | 단계마다 체크리스트 |
|
||||||
|
| 16:20~16:44 | 4 구현: **엔진 1단계 → 엔진 2단계 → TUI 1단계** (막히면 5) | 단계마다 `pytest` · 직접 실행 · commit |
|
||||||
|
| 16:44~16:50 | 6 확인 · 7 `uv tool install` · 8 다른 폴더에서 | `tlog summary --url …` = `my-log-report`의 숫자 |
|
||||||
|
|
||||||
|
50분 안 목표: **R1 · R2 · R5 · R12 · R13(엔진 1단계) + R3 · R4 · R7(엔진 2단계) + R11(TUI 1단계) + R14(설치)**. R6 · R8 · R9 · R10은 IMP-PLAN의 다음 단계로 8교시 뒤 · 집에서 이어 갑니다.
|
||||||
|
|
||||||
|
## 0. 준비 (1분)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/vibe/tlog/Doc && cd ~/vibe/tlog
|
||||||
|
git init
|
||||||
|
# 에이전트는 Doc/TODO.md를 쓴 뒤에 켭니다: codex (또는 claude, agy)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. `Doc/TODO.md` · 만들고 싶은 것과 할 일 정리 (8분, 에이전트 없이)
|
||||||
|
|
||||||
|
[VIBE-REQ-TLOG.md](VIBE-REQ-TLOG.md)의 "할 일" 틀대로 씁니다. 요청의 말 ①~⑫를 **내 말로** 옮기고, 꼭 · 나중을 표시하고, 걱정되는 것과 50분 안에 못 할 것을 적습니다. "어떻게"는 아직 쓰지 않습니다.
|
||||||
|
|
||||||
|
<details><summary>▶ TODO와 SPEC은 무엇이 다른가</summary>
|
||||||
|
|
||||||
|
TODO는 **내가 이해한 것**(무엇을 · 왜)입니다. SPEC은 그것을 **확인할 수 있게** 바꾼 것(됐다 · 안 됐다를 말하는 방법)입니다. 오늘 확인 방법은 대부분 **숫자**입니다: 두 주 666 · 21 · 0.0315 · 1205 · 2537처럼, n8n이 낸 숫자와 같은지.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
`git add Doc/TODO.md && git commit -m "TODO"`
|
||||||
|
|
||||||
|
## 2. `Doc/SPEC.md` · 확인할 수 있는 요구로 (6분)
|
||||||
|
|
||||||
|
TODO의 할 일마다 **확인 방법**(기대 숫자 · 종료 코드 · JSON 칸)을 붙여 SPEC을 만듭니다. 에이전트에게 "TODO.md를 읽고 SPEC.md 초안을 써 줘. 요구마다 확인 방법을 붙여"라고 시켜도 됩니다. 그러면 **반드시 내가 읽고 고칩니다.**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# tlog SPEC
|
||||||
|
|
||||||
|
## 1. 요구 (확인할 수 있게)
|
||||||
|
| 번호 | 요구 | 요청의 어느 말 | 확인 방법 | 꼭 · 나중 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| R1 | | ① | | 꼭 |
|
||||||
|
|
||||||
|
## 2. 숨은 요구
|
||||||
|
## 3. 범위 밖 (하지 않을 것)
|
||||||
|
## 4. 정해 둘 것 (지표 정의 = AGENTS.md 3절, 반올림 방법, 종료 코드 번호, 기본 SLO, 읽을 파일 모양)
|
||||||
|
```
|
||||||
|
|
||||||
|
<details><summary>▶ 확인 방법은 n8n이 낸 숫자로</summary>
|
||||||
|
|
||||||
|
"오류율을 보여 준다"는 확인 방법이 아닙니다. "두 주(10/19~10/30) → `error_rate` 0.0315, 둘째 주 → 0.0588, 10/26 하루 → 실행 70 · 오류 16, 빈 기간 → 0 (null 아님)"이 확인 방법입니다. 오늘 Lab 01의 `cases/lab01.json` · `lab01-edges.json`에 있는 숫자를 그대로 가져오면 됩니다 — 정의가 같으면 숫자도 같아야 합니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
쓴 뒤 아래 표와 비교합니다. 빠진 요구, 내가 더 뽑은 요구를 `Doc/SPEC.md` 끝 "VIBE-Q와 비교"에 한 줄씩 적고 SPEC을 고칩니다.
|
||||||
|
|
||||||
|
### 요구 사항 (요청서에서 뽑은 것, 14개)
|
||||||
|
|
||||||
|
| 번호 | 요구 | 요청서의 말 | 확인 방법 | 50분 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| R1 | `--file`(JSON 목록 · `{"logs": [...]}` · JSON Lines) 또는 `--url`(데이터 API `POST /logs/query`)로 읽기, `--from` · `--to`(날짜 **포함**) · `--tag` · `--source`로 거르기 | ① | 파일 · API 둘 다 10/26 하루 → 70줄, `--source live` → 오늘 기록만 | 꼭 |
|
||||||
|
| R2 | `tlog summary`: `total` · `errors` · `error_rate`(소수 4자리, JavaScript `Math.round`처럼) · `p50_ms` · `p95_ms`(`ceil(p × n)`번째) · `by_route`(`null` → `오류`) · `by_day` · `by_release`, 0건이면 0 | ② | 두 주 666 · 21 · 0.0315 · 1205 · 2537 · 갈래 591 · 34 · 20 · 21, 빈 기간 0 · 0 · 0 | 꼭 |
|
||||||
|
| R3 | `tlog problems`: `failures`(노드 · 메시지, 많은 순) · `unanswered_topics` · `repeat_topics`(비율 높은 순) | ③ | 두 주 → Search FAQ timeout 16 · Answer 429 5, 와이파이 34, 프린터 9.88% | 꼭 |
|
||||||
|
| R4 | `anomalies`: 시간마다, 5건 이상 · 그 기간 오류율의 3배 이상 · 오류 1건 이상, 시간순 | ④ | 두 주 → 10/26 09시 · 10/29 15시, 첫 주 → 없음 | 꼭 |
|
||||||
|
| R5 | `--json`: AGENTS.md 3절과 **같은 칸 이름**으로 | ⑤ · ② | 같은 기간 `my-log-report` 응답과 칸을 하나씩 비교 → 다른 칸 0개 | 꼭 |
|
||||||
|
| R6 | `steps`(단계마다 값 > 0인 실행만 `{runs, p50, p95}`) · `cost_usd` · `cost_per_run` · `llm_calls` | ⑥ | 두 주 FAQ p95 39 (646) · 답 p95 2509 (596) · 0.07836 · 0.000118 · 596, 10/26 FAQ p95 1000 | 나중 |
|
||||||
|
| R7 | `tlog slo [--error-rate] [--p95] [--unanswered]`: 기준(기본 0.01 · 3000 · 0.02)보다 **크면** 위반, 위반이면 종료 코드 4 | ⑦ | 둘째 주 → `error_rate, unanswered_rate` · 종료 코드 4, 첫 주 `--unanswered 0.06` → 0 | 꼭 |
|
||||||
|
| R8 | `tlog compare --prev-from --prev-to`: 지표를 나란히 + 차이 | ⑧ | 첫 주 → 둘째 주: 실행 309 → 357 (+48), p95 2525 → 2598 | 나중 |
|
||||||
|
| R9 | 76일차 `tretry` 기록(시도마다 한 줄)을 실행 한 줄 모양으로 바꿔 읽기 | ⑨ | 시도 7줄(실행 3번: 다 실패 · 성공 · 대체) → 실행 3 · 오류 1 · 갈래 `성공` · `대체` | 나중 |
|
||||||
|
| R10 | `tlog weekly --prev-from --prev-to`: 마크다운 (숫자 표 · 문제 · 할 일 규칙, LLM 없음) | ⑩ | `\| 요청 \| 309 \| 357 \|` · `2026-10-26 09시 · 16/16` · `FAQ에 '와이파이' 항목 추가` | 나중 |
|
||||||
|
| R11 | `tlog ui`: 위 요약 한 줄, 왼쪽 실패 묶음(`[t]`로 시간대 · 느린 실행), 고르면 오른쪽에 그 실행들, `[r]` 다시 읽기 | ⑪ | `run_test`: 첫 줄이 16건 묶음 · 오른쪽 16줄, `down` → 5줄, `t` → 첫 줄 `2026-10-26T09` | 꼭 |
|
||||||
|
| R12 | 종료 코드 0 됨 · 1 로그를 못 읽음 · 2 쓰는 법이 틀림 · 4 SLO 위반 | ⑫ | 출처 없음 → 2, 없는 파일 → 1, 둘째 주 `slo` → 4 | 꼭 |
|
||||||
|
| R13 | 테스트는 n8n · 데이터 API 없이: 두 주 기록의 사본, 테스트 안의 가짜 데이터 API | 끝 | `uv run pytest -q`가 10초 안, 두 주 숫자 = n8n 숫자 | 꼭 |
|
||||||
|
| R14 | `uv tool install`로 설치해 **어느 폴더에서나** `tlog` | 첫 문단 | `cd /tmp && tlog --help`, `uv tool list`에 `tlog` | 꼭 |
|
||||||
|
|
||||||
|
숨은 요구 (요청서에 직접 없지만 없으면 안 되는 것):
|
||||||
|
|
||||||
|
| 숨은 요구 | 어디서 나왔나 |
|
||||||
|
| --- | --- |
|
||||||
|
| **정의는 한 곳**(엔진 모듈 하나). 화면 · JSON · SLO · 주간 보고는 그 함수를 부르기만 | ② — 정의가 두 곳이면 오늘 팀 보고서처럼 어긋남 |
|
||||||
|
| 파일에서 거르는 규칙 = 데이터 API가 거르는 규칙 (끝날 포함) | ① |
|
||||||
|
| 반올림을 n8n(JavaScript)과 같게 (half up) | ② — Python `round`는 짝수 쪽으로 반올림 |
|
||||||
|
| 빈 기간 · 0건에서 `null` · 0으로 나누기 오류가 없게 | ② · 오늘 Lab 01 예제 3 |
|
||||||
|
| 반쯤 쓰인 JSON Lines 줄은 건너뛰기 | ⑨ — tretry가 기록 중에 죽었을 때 |
|
||||||
|
| 읽는 층(`load`) · 세는 층(`metrics`) · 보여 주는 층(cli · TUI)을 나눔 | ⑨ · ⑪ — 기록 모양이 늘어도 세는 곳은 그대로 |
|
||||||
|
|
||||||
|
범위 밖 (오늘 하지 않음): LLM 요약(오늘 n8n Lab 02 과제 2에서 함), n8n 공개 API에서 실행 기록 가져오기(선택), 알림 보내기(오늘 n8n Lab 03 과제 2), 실시간 따라 읽기(`tail -f`).
|
||||||
|
|
||||||
|
`git add Doc/SPEC.md && git commit -m "SPEC"`
|
||||||
|
|
||||||
|
## 3. `Doc/IMP-PLAN.md` · 구현 계획 (5분)
|
||||||
|
|
||||||
|
SPEC의 요구를 **단계**로 나눕니다. 첫 원칙: **엔진(세는 곳)을 먼저, 화면(TUI)은 나중에.** 화면은 엔진 함수를 불러 표에 넣기만 하면 되게 만듭니다. 에이전트에게 "SPEC.md를 읽고 IMP-PLAN.md를 엔진 단계와 TUI 단계로 나눠 써 줘. 단계마다 체크리스트와 확인 명령"이라고 시켜도 됩니다 (내가 읽고 고침).
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# tlog IMP-PLAN
|
||||||
|
|
||||||
|
## 엔진 1단계 · 읽기 · 정의대로 세기 · JSON · 종료 코드 (R1 · R2 · R5 · R12 · R13)
|
||||||
|
- [ ] load.py: read(file, url, from, to, tag, source), 날짜 포함, JSON · JSON Lines, 데이터 API는 POST /logs/query
|
||||||
|
- [ ] metrics.py: pct(nearest rank) · rate(4자리, half up, 0건 0) · summary(rows)
|
||||||
|
- [ ] cli: tlog summary [--json], 종료 코드 0 · 1 · 2
|
||||||
|
- [ ] tests: 두 주 사본 → 666 · 21 · 0.0315 · 1205 · 2537, 빈 기간 0, 가짜 데이터 API
|
||||||
|
- 확인: uv run pytest -q, uv run tlog summary --file … --from 2026-10-19 --to 2026-10-30
|
||||||
|
|
||||||
|
## 엔진 2단계 · 문제 · 이상 · SLO (R3 · R4 · R7)
|
||||||
|
## TUI 1단계 · 묶음 고르기 (R11)
|
||||||
|
## 설치 (R14)
|
||||||
|
## 엔진 3단계 · 단계 · 비용 · 비교 · tretry · 주간 보고 (R6 · R8 · R9 · R10)
|
||||||
|
```
|
||||||
|
|
||||||
|
<details><summary>▶ 왜 엔진 먼저인가</summary>
|
||||||
|
|
||||||
|
- 엔진은 **화면 없이 테스트**할 수 있습니다 (`pytest`, `tlog summary`). 숫자가 n8n과 같은지가 오늘의 핵심인데, 화면부터 만들면 눈으로만 봅니다.
|
||||||
|
- 밤마다 돌릴 SLO 판정은 화면 없이 돌아갑니다. 종료 코드(R12)가 cron · CI의 다음 일을 정합니다 (74일차 게이트와 같음).
|
||||||
|
- TUI는 엔진의 묶음을 표에 넣기만 합니다. 묶는 규칙(R3 · R4)이 정해져야 TUI를 만들 수 있습니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 단계 크기</summary>
|
||||||
|
|
||||||
|
한 단계 = 에이전트 요청 한두 번 + 확인 한 번 + commit 한 번 (10분 안팎). 오늘 Lab에서 보고서를 한 번에 다 만들지 않고 "지표 → 날짜 · 릴리스 → 단계 · 비용 → 묶음 → 이상 → SLO"로 한 칸씩 더한 것과 같습니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
`git add Doc/IMP-PLAN.md && git commit -m "IMP-PLAN"`
|
||||||
|
|
||||||
|
## 4. 구현 시작 (24분: 엔진 1단계 → 엔진 2단계 → TUI 1단계)
|
||||||
|
|
||||||
|
이제 에이전트를 켭니다. 단계마다 긴 요청문은 필요 없습니다. 할 일은 SPEC · IMP-PLAN에 있으니까요.
|
||||||
|
|
||||||
|
**처음 한 번:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 체크 안 된 첫 단계만 만들어 줘. uv 프로젝트, 명령 이름 tlog, 코드와 화면 글자는 영어.
|
||||||
|
끝나면 IMP-PLAN 체크를 표시하고 확인 명령을 알려 줘. 다음 단계는 내가 말할 때까지 하지 마.
|
||||||
|
```
|
||||||
|
|
||||||
|
**그다음 단계마다 (같은 한 줄):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
확인했고 commit 했어. 다음 단계.
|
||||||
|
```
|
||||||
|
|
||||||
|
→ 내가 확인 → commit → 한 줄. 그냥 "계속"만 치면 여러 단계를 한꺼번에 하거나 체크 · 확인 명령을 빼먹기 쉽습니다. 틀렸을 때만 **무엇이 틀렸는지 · SPEC의 어느 요구인지 · 어디만 고칠지 · 무엇으로 확인할지**를 적어 고쳐 달라고 합니다. 대화가 길어지면 새 세션에서 처음 요청을 다시 보냅니다.
|
||||||
|
|
||||||
|
> **규모가 큰 프로젝트일수록 다단계로** 진행합니다. `tlog`도 요구가 14개라 한 번에 시키면 에이전트가 수백 줄을 한꺼번에 쓰고, 숫자가 n8n과 다를 때 어디가 다른지 찾기 어렵습니다. 단계마다 ① 그 단계만 요청 ② 직접 확인 ③ IMP-PLAN 체크 ④ `git commit`.
|
||||||
|
|
||||||
|
<details><summary>▶ 엔진 1단계 · 같은 숫자가 나오는지 어떻게 보나</summary>
|
||||||
|
|
||||||
|
두 주 기록의 사본(`~/aio-01d4-stu/Last3Mon/77/data-api/data/run-logs.json`을 `tests/data/`에 복사)으로 테스트를 쓰고, 기대 숫자는 오늘 `cases/lab01.json`에서 가져옵니다. 수업 중이라면 n8n에도 물어 봅니다: `uv run ~/aio-01d4-stu/Last3Mon/77/tools/flow.py call my-log-report requests/week2.json`과 `tlog summary --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30 --json`.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 엔진 1단계 · 반올림이 1 다를 때</summary>
|
||||||
|
|
||||||
|
0.03153…은 어느 방법으로도 0.0315지만, 정확히 반에 걸린 값은 Python `round`(짝수 쪽)와 JavaScript `Math.round`(위로)가 다릅니다. n8n Code 노드는 JavaScript이므로 `floor(x × 10^d + 0.5) / 10^d`로 맞춥니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 엔진 2단계 · 이상 시간대의 "평소"</summary>
|
||||||
|
|
||||||
|
"전체 오류율의 3배"의 전체는 **그 기간의** 오류율입니다 (오늘 Lab 02 예제 3). 그리고 오류율이 0인 기간에서 3배(0) 조건만으로 모든 시간이 뽑히지 않게 "오류 1건 이상"을 함께.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 엔진 2단계 · SLO 위반의 종료 코드</summary>
|
||||||
|
|
||||||
|
"프로그램이 실패했다"(1 · 2)와 "SLO를 어겼다"(4)를 다른 번호로 둡니다. cron이 1이면 "도구를 고쳐라", 4면 "서비스를 봐라"로 다르게 알릴 수 있습니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ TUI 1단계 · 화면 나누기</summary>
|
||||||
|
|
||||||
|
묶기(엔진)와 화면을 나눕니다 (`tretry` · `tcost`와 같음). 왼쪽 표의 줄을 고르면(`RowHighlighted`) 오른쪽 표를 그 묶음의 실행으로 다시 채웁니다. `[t]`는 왼쪽 표만 다른 묶음(시간대 · 느린 실행)으로 바꿉니다.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 다음 단계들 (8교시 뒤 · 집에서)</summary>
|
||||||
|
|
||||||
|
- 단계 · 비용(R6): 단계 값이 0인 실행은 뺌 (오늘 Lab 01 과제 2).
|
||||||
|
- 비교(R8): 같은 함수로 두 기간을 세고 칸마다 차이.
|
||||||
|
- tretry(R9): 시도 줄을 `final`까지 묶어 실행 하나로. 성공 · 대체 · 실패를 갈래로, 시간은 마지막 시도의 시작 + 걸린 시간.
|
||||||
|
- 주간 보고(R10): 오늘 n8n Lab 03 과제 1의 마크다운과 같은 모양, 요약 절 없이.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 5. 바이브 코딩으로 문제 해결 팁
|
||||||
|
|
||||||
|
| 이럴 때 | 이렇게 |
|
||||||
|
| --- | --- |
|
||||||
|
| 에이전트가 "다 됐다"고 함 | **직접 실행**합니다 (`uv run tlog summary --file … --from … --to …`, `uv run pytest -q`). 확인 방법은 SPEC에 있습니다 |
|
||||||
|
| 숫자가 n8n과 1~2 다름 | "AGENTS.md 3절 정의 그대로: 끝날 포함, `ceil(p × n)`번째, 소수 4자리 half up" — 어느 칸이 다른지 칸 이름과 두 값을 붙여 줍니다 |
|
||||||
|
| 빈 기간에 `ZeroDivisionError` · `null` | "0건이면 비율 · 백분위 · 실행당 비용 모두 0" |
|
||||||
|
| 파일과 API의 숫자가 다름 | "파일 거르기 규칙을 데이터 API와 같게 (from ≤ started_at[:10] ≤ to)" |
|
||||||
|
| 테스트가 데이터 API를 부름 | "테스트 안의 가짜 서버(http.server)로, 진짜 API는 확인 단계에서만" |
|
||||||
|
| 세는 코드가 cli · 화면에 흩어짐 | "metrics.py 하나에, 나머지는 부르기만" |
|
||||||
|
| `slo`가 늘 0 | "기준보다 크면 위반(>), 위반이면 종료 코드 4" |
|
||||||
|
| tretry 기록이 시도마다 실행으로 셈 | "final 줄까지 묶어 실행 하나로" |
|
||||||
|
| 화면이 이상한데 오류가 안 보임 | `uv add --dev textual-dev` 뒤 다른 터미널에서 `uv run textual console` |
|
||||||
|
| 같은 곳을 3번 고쳐도 안 됨 | 멈추고 `git diff` · `git checkout .`로 되돌린 뒤, 단계를 **더 작게** 나눠 다시 요청 |
|
||||||
|
| 설치한 `tlog`가 고친 내용을 모름 | `uv tool install --force .` 로 다시 설치 (또는 처음부터 `--editable`) |
|
||||||
|
|
||||||
|
## 6. 결과 확인
|
||||||
|
|
||||||
|
`Doc/SPEC.md` 1절 표의 확인 방법대로 **내가** 하나씩 하고 체크합니다. 오늘 50분 안 목표만:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/vibe/tlog
|
||||||
|
uv run pytest -q # R13: 10초 안
|
||||||
|
F=~/aio-01d4-stu/Last3Mon/77/data-api/data/run-logs.json
|
||||||
|
uv run tlog summary --file $F --from 2026-10-19 --to 2026-10-30 # R1 · R2: 666 · 21 · 3.15% · 1205 · 2537
|
||||||
|
uv run tlog summary --url http://localhost:8800 --from 2026-10-26 --to 2026-10-26 # R1: 70 · 16 (API)
|
||||||
|
uv run tlog summary --file $F --from 2026-11-02 --to 2026-11-06 # R2: 0 runs (null 아님)
|
||||||
|
uv run tlog problems --file $F --from 2026-10-19 --to 2026-10-30 # R3 · R4
|
||||||
|
uv run tlog slo --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30; echo "exit $?" # R7: 4
|
||||||
|
uv run tlog summary; echo "exit $?" # R12: 2
|
||||||
|
uv run tlog summary --url http://localhost:8800 --from 2026-10-26 --to 2026-10-30 --json > /tmp/tlog.json
|
||||||
|
(cd ~/aio-01d4-stu/Last3Mon/77 && uv run tools/flow.py call my-log-report requests/week2.json) # R5: 숫자 비교
|
||||||
|
uv run tlog ui --url http://localhost:8800 --from 2026-10-19 --to 2026-10-30 # R11
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] R1 파일 · API, 날짜 포함.
|
||||||
|
- [ ] R2 n8n과 같은 숫자, 빈 기간 0.
|
||||||
|
- [ ] R3 실패 묶음 · 답 못 한 주제 · 다시 묻는 주제.
|
||||||
|
- [ ] R4 이상 시간대.
|
||||||
|
- [ ] R5 `--json` 칸 이름 · 값이 `my-log-report`와 같음.
|
||||||
|
- [ ] R7 SLO · 종료 코드 4.
|
||||||
|
- [ ] R11 TUI.
|
||||||
|
- [ ] R12 종료 코드 0 · 1 · 2 · 4.
|
||||||
|
- [ ] R13 n8n 없이 도는 테스트.
|
||||||
|
- [ ] (이어서) R6 · R8 · R9 · R10.
|
||||||
|
|
||||||
|
## 7. `uv tool install`로 설치
|
||||||
|
|
||||||
|
`pyproject.toml`에 명령이 등록되어 있는지 먼저 봅니다.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.scripts]
|
||||||
|
tlog = "tlog.cli:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/vibe/tlog
|
||||||
|
uv tool install --editable . # 고친 내용이 바로 반영됨 (수업 중 권함)
|
||||||
|
uv tool list # tlog 가 보이는가
|
||||||
|
which tlog # ~/.local/bin/tlog
|
||||||
|
# 명령을 못 찾으면: uv tool update-shell 뒤 터미널을 새로 엶
|
||||||
|
```
|
||||||
|
|
||||||
|
- 지우기: `uv tool uninstall tlog`.
|
||||||
|
|
||||||
|
## 8. 다른 곳에서 사용
|
||||||
|
|
||||||
|
설치했으면 오늘 Lab과 내 기록에 씁니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
export TLOG_URL=http://localhost:8800 # --url 을 매번 쓰지 않게
|
||||||
|
tlog summary --tag after --source live # 오늘 Lab 03에서 고친 뒤 기록
|
||||||
|
tlog slo --tag after --source live; echo "exit $?" # (ops --reset 전이면) 고친 뒤 p95 위반 → 4
|
||||||
|
tlog compare --from 2026-10-26 --to 2026-10-30 --prev-from 2026-10-19 --prev-to 2026-10-23 # (R8)
|
||||||
|
tlog weekly --from 2026-10-26 --to 2026-10-30 --prev-from 2026-10-19 --prev-to 2026-10-23 > ~/weekly.md # (R10)
|
||||||
|
tlog summary --file ~/.tretry/log.jsonl # (R9) 76일차 tretry 기록
|
||||||
|
# 밤마다: 0 3 * * * tlog slo --from $(date +%F) --to $(date +%F) || echo "SLO" | mail ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `~/vibe/tlog`는 지우지 마세요. 79일차 `tops`에서 다시 씁니다.
|
||||||
|
|
||||||
|
## 발표 (8교시, 3분)
|
||||||
|
|
||||||
|
| 내용 | |
|
||||||
|
| --- | --- |
|
||||||
|
| TODO → SPEC | 내 SPEC과 VIBE-Q 요구 표가 달랐던 곳 (빠뜨린 요구, 더 뽑은 요구) |
|
||||||
|
| IMP-PLAN | 엔진 · TUI를 어떻게 나눴나, 어디까지 체크했나 |
|
||||||
|
| 요청문 | 처음 요청 · 틀렸을 때 고쳐 달라고 한 요청문 하나 |
|
||||||
|
| 막힌 곳 | 5절의 어느 팁으로 풀었나 (숫자가 다름 · 빈 기간 · 반올림 · 테스트가 API를 부름) |
|
||||||
|
| 시연 | `tlog summary` 와 `my-log-report` 의 숫자 비교, `tlog slo` 종료 코드, `tlog ui` |
|
||||||
|
|
||||||
|
## 확인
|
||||||
|
|
||||||
|
- [ ] `Doc/TODO.md` → `Doc/SPEC.md` → `Doc/IMP-PLAN.md`를 에이전트 전에 (또는 에이전트와 함께 쓰고 내가 고쳐서) 만들고 commit했다.
|
||||||
|
- [ ] IMP-PLAN의 엔진 1 · 2단계와 TUI 1단계를 체크했다 (단계마다 commit).
|
||||||
|
- [ ] R1 · R2 · R3 · R4 · R5 · R7 · R11 · R12 · R13 확인, `uv run pytest -q`, n8n과 같은 숫자.
|
||||||
|
- [ ] R14 `uv tool install`, 다른 폴더에서 `tlog`.
|
||||||
|
- [ ] 남은 요구는 IMP-PLAN에 체크 안 된 채로 남겨 두었다 (다음에 이어 감).
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# 7교시 바이브 코딩 · 요청서 `tlog` (가장 먼저 읽기)
|
||||||
|
|
||||||
|
- 순서: **이 요청서** → 내 정리 `~/vibe/tlog/Doc/TODO.md` (8분) → [VIBE-Q-TLOG.md](VIBE-Q-TLOG.md)의 순서대로 `Doc/SPEC.md` → `Doc/IMP-PLAN.md` → 단계별로 만들기 → 설치 · 다른 곳에서 쓰기 → 수업이 끝나면 `VIBE-A-TLOG.md`(정답본)로 확인
|
||||||
|
- 이 문서는 **만들어 달라는 사람의 말**입니다. 요구 사항 표도, 단계도 없습니다. 그것을 뽑아 내는 것이 첫 일입니다. 72일차 `tbench`부터 76일차 `tretry`까지와 같은 순서입니다.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
REQ["요청서<br/>(사람의 말)"] --> TODO["Doc/TODO.md<br/>만들고 싶은 것 ·<br/>요청의 말 정리"] --> SPEC["Doc/SPEC.md<br/>확인할 수 있는 요구"] --> PLAN["Doc/IMP-PLAN.md<br/>엔진 → TUI 단계"] --> RUN["단계별로 만들기 ·<br/>직접 확인"] --> USE["uv tool install<br/>다른 곳에서 쓰기"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **생활 속 예 · 주머니 속 계산기와 장부.** 오늘 가게 장부(n8n 보고서)를 만들었습니다. 그런데 사장님이 밤에 집에서 "어제 몇 시에 문제가 있었지?"를 보려면 가게 컴퓨터를 켜야 합니다. 같은 장부 쓰는 법으로 **주머니 속 계산기**에서도 같은 숫자가 나오면 어디서든 볼 수 있습니다. `tlog`는 오늘 만든 보고서와 **같은 정의**로 터미널에서 세는 계산기입니다.
|
||||||
|
|
||||||
|
## 왜 요청서부터인가
|
||||||
|
|
||||||
|
요구 사항 표를 통째로 에이전트에게 주고 "이대로 만들어 줘"라고 하면, 무엇이 만들어지든 **내가 판단할 기준이 없습니다.** 실무에서는 요구 사항 표가 오지 않고, 이 요청서 같은 **사람의 말**이 옵니다. 오늘 Lab 01 예제 1에서 팀의 보고서와 숫자를 먼저 비교해 본 것과 같습니다: 무엇이 맞는 숫자인지 정해야 맞았는지 말할 수 있습니다.
|
||||||
|
|
||||||
|
| 단계 | 누가 | 결과 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 요청 → 할 일 목록 | **나** | `Doc/TODO.md`: 요청의 말 ①~⑫를 내 말로, 만들고 싶은 것 |
|
||||||
|
| 할 일 → 요구 | **나** (에이전트와 다듬어도 됨) | `Doc/SPEC.md`: 요구마다 "됐다 / 안 됐다"를 말할 수 있는 확인 방법 |
|
||||||
|
| 요구 → 단계 | **나** (에이전트와 다듬어도 됨) | `Doc/IMP-PLAN.md`: 엔진 단계와 화면(TUI) 단계로 나눈 순서 |
|
||||||
|
| 단계 → 코드 | 에이전트 | 처음 요청 한 번 + 단계마다 한 줄, `Doc/*.md`를 읽게 함 |
|
||||||
|
| 확인 | **나** | 직접 `uv run tlog …`, `pytest`, n8n `my-log-report`와 숫자 비교, 요구 표에 체크 |
|
||||||
|
|
||||||
|
## 요청 (운영 담당에게서 온 메시지, 그대로)
|
||||||
|
|
||||||
|
> 오늘 n8n으로 로그 보고서 · 문제 찾기 · SLO · 주간 보고까지 만들었잖아요. 정말 좋은데, 밤에 집에서 노트북으로 "아까 몇 시에 무슨 일이 있었지?"를 보려면 매번 n8n을 켜고 웹훅을 불러야 해요. 그리고 n8n 밖에도 로그가 있어요. 76일차에 만든 `tretry` 기록 같은 거요. **어느 폴더에서나** `tlog`라고 치면 로그를 볼 수 있는 작은 도구가 있으면 좋겠어요.
|
||||||
|
>
|
||||||
|
> ① 로그를 **파일에서도**, **데이터 API에서도** 읽어 주세요. 기간(시작일 · 끝일)과 태그 · 출처로 거를 수 있게요. 끝일도 **넣어서** 세 주세요. 오늘 팀 보고서가 끝날을 빼서 큰일 날 뻔했잖아요.
|
||||||
|
>
|
||||||
|
> ② 실행 수 · 오류 수 · 오류율 · p50 · p95, 갈래마다 수를 보여 주세요. **오늘 n8n 보고서와 똑같은 숫자**가 나와야 해요. 다르면 회의에서 또 "어느 숫자가 맞나"로 싸워요.
|
||||||
|
>
|
||||||
|
> ③ 실패를 **원인으로 묶어서** 많은 순으로, 그리고 사람에게 넘긴 질문의 주제, 답했는데 다시 묻는 주제도요.
|
||||||
|
>
|
||||||
|
> ④ **평소보다 크게 나빴던 시간**을 찾아 주세요. 오늘 Lab 02에서 정한 그 규칙으로요.
|
||||||
|
>
|
||||||
|
> ⑤ 다른 프로그램이 읽을 수 있게 **JSON으로도** 주세요. 칸 이름은 n8n 보고서와 같게요. 그래야 둘을 바로 비교할 수 있어요.
|
||||||
|
>
|
||||||
|
> ⑥ 어느 **단계**가 느린지, LLM에 **얼마** 들었는지도 보고 싶어요.
|
||||||
|
>
|
||||||
|
> ⑦ **SLO를 지켰는지** 판정해 주세요. 기준은 바꿀 수 있게요. 그리고 이걸 밤마다 자동으로 돌릴 거라, 어기면 **종료 코드로** 알려 주세요. 그래야 cron이나 CI가 알람을 울려요.
|
||||||
|
>
|
||||||
|
> ⑧ **지난주와 이번 주를 나란히** 보고 싶어요. 얼마나 늘고 줄었는지요.
|
||||||
|
>
|
||||||
|
> ⑨ 76일차 **`tretry` 기록**(`~/.tretry/log.jsonl`)도 읽어 주세요. 모양이 다르지만 같은 방식으로 보고 싶어요.
|
||||||
|
>
|
||||||
|
> ⑩ 오늘 만든 **주간 보고**처럼 마크다운으로 뽑아 주세요. 여기서는 LLM 없이 숫자 · 문제 · 할 일만이면 돼요.
|
||||||
|
>
|
||||||
|
> ⑪ **화면에서** 보고 싶어요. 위에 요약 한 줄, 왼쪽에 실패 묶음 — 하나를 고르면 오른쪽에 그 실행들이 보이게요. 실패 묶음 말고 시간대별 · 느린 실행으로도 바꿔 볼 수 있게요.
|
||||||
|
>
|
||||||
|
> ⑫ 스크립트에서 쓸 거라 **종료 코드**가 중요해요. 됐다 · 로그를 못 읽었다 · 쓰는 법이 틀렸다 · SLO를 어겼다를 서로 다른 번호로요.
|
||||||
|
>
|
||||||
|
> 아, 시험할 때 **진짜 n8n이나 데이터 API가 없어도** 돌아가야 해요. 그리고 오늘 두 주 기록으로 **n8n과 같은 숫자인지** 꼭 확인해 주세요.
|
||||||
|
|
||||||
|
## 함께 받은 것
|
||||||
|
|
||||||
|
| 파일 · 자료 | 메모 |
|
||||||
|
| --- | --- |
|
||||||
|
| 오늘 데이터 API `http://localhost:8800` | `POST /logs/query` 본문 `{"from", "to", "tag", "source"}` → `{"count", "logs"}` (날짜 포함) |
|
||||||
|
| `~/aio-01d4-stu/Last3Mon/77/data-api/data/run-logs.json` | 두 주 기록 666줄 (파일로 읽기 · 테스트용 사본) |
|
||||||
|
| [AGENTS.md](AGENTS.md) 2 · 3절 | 기록 한 줄의 모양, **지표 정의** (오늘의 정의서) |
|
||||||
|
| 내 `my-log-report` | 같은 기간을 물어 숫자를 비교할 상대: `uv run tools/flow.py call my-log-report requests/week2.json` |
|
||||||
|
| 76일차 `tretry` 기록 | `~/.tretry/log.jsonl` (없으면 `tretry --tries 2 --backoff 0 -- false`로 한 줄 만들기) |
|
||||||
|
| Python 표준 라이브러리 | `json`, `urllib.request`. TUI는 Textual |
|
||||||
|
|
||||||
|
## 할 일 · 요청을 내 할 일 목록으로 (8분, 에이전트 없이)
|
||||||
|
|
||||||
|
`~/vibe/tlog/Doc/TODO.md`를 만들고 요청의 말 ①~⑫와 맨 끝 부탁을 **내 말로** 한 줄씩 옮깁니다. 아직 "어떻게"는 쓰지 않습니다. **무엇을 · 왜**만 씁니다. 에이전트에게 맡기지 않고 직접 씁니다.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# tlog TODO
|
||||||
|
|
||||||
|
## 만들고 싶은 것 (한 문단)
|
||||||
|
어느 폴더에서나 `tlog`로 실행 기록(파일 · 데이터 API)을 오늘 n8n 보고서와 같은 정의로 세어 ...
|
||||||
|
|
||||||
|
## 요청의 말 → 할 일
|
||||||
|
| # | 요청의 말 (짧게) | 할 일 (내 말) | 꼭 · 나중 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| ① | 파일 · API, 기간 · 태그, 끝날 포함 | | 꼭 |
|
||||||
|
| ② | n8n과 똑같은 숫자 | | |
|
||||||
|
| ... | | | |
|
||||||
|
| 끝 | n8n 없이 시험 · 같은 숫자 확인 | | |
|
||||||
|
|
||||||
|
## 요청에 없지만 걱정되는 것
|
||||||
|
## 오늘 50분 안에 못 할 것 같은 것
|
||||||
|
```
|
||||||
|
|
||||||
|
예 하나만 보여 드립니다.
|
||||||
|
|
||||||
|
| 요청의 말 | TODO에 쓸 할 일 (내 말) | 다음 단계(SPEC)에서 바뀔 모양 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ② "오늘 n8n 보고서와 똑같은 숫자" | "AGENTS.md 3절 정의 그대로: 끝날 포함, 소수 4자리, p95는 `ceil(p × n)`번째, 오류 갈래" | R2: 두 주 → 666 · 21 · 0.0315 · p50 1205 · p95 2537, 같은 기간 `my-log-report` 응답과 칸이 모두 같음 |
|
||||||
|
|
||||||
|
<details><summary>▶ 1단계 · 꼭 할 것과 나중에 할 것</summary>
|
||||||
|
|
||||||
|
요청의 말이 열두 개입니다. 50분 안에 다 만들기 어렵습니다. 담당자가 가장 아파하는 것은 "n8n과 똑같은 숫자"(회의에서 싸우지 않기)와 "밤마다 자동으로 SLO"입니다. 그리고 문제 찾기 · 화면 · 주간 보고를 만들려면 먼저 무엇이 있어야 하나요? (읽기 → 정의대로 세기 → JSON → 묶기 · 이상 → SLO → 화면)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>▶ 2단계 · 걱정되는 것 (숨은 요구의 씨앗)</summary>
|
||||||
|
|
||||||
|
- ① "끝일도 넣어서": 파일에서 읽을 때는 내가 거르고, 데이터 API에서 읽을 때는 API가 거릅니다. 두 곳의 규칙이 같아야 합니다.
|
||||||
|
- ② "똑같은 숫자": 반올림은 어떻게? Python의 `round(0.5)`는 0, JavaScript의 `Math.round(0.5)`는 1입니다. n8n의 Code 노드는 JavaScript입니다. p95의 계산 방법(보간이냐 순위냐)도.
|
||||||
|
- ② 기록이 0건이면? 오늘 팀 보고서는 `null`을 냈습니다.
|
||||||
|
- ⑥ "단계": 그 단계를 거치지 않은 실행(0ms)을 넣을까요?
|
||||||
|
- ⑦ "종료 코드로": SLO 위반은 "프로그램이 실패한 것"과 다른 번호여야 cron이 둘을 구분합니다.
|
||||||
|
- ⑨ "모양이 다르지만": `tretry`는 시도마다 한 줄, 헬프데스크는 실행마다 한 줄입니다. 읽을 때 **같은 모양으로 바꾸는 층**을 하나 둡니다.
|
||||||
|
- 끝 "n8n 없이 시험": 데이터 API를 대신할 작은 가짜 서버, 두 주 기록의 사본.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 그다음
|
||||||
|
|
||||||
|
[VIBE-Q-TLOG.md](VIBE-Q-TLOG.md)를 엽니다. 거기서 `Doc/TODO.md` → `Doc/SPEC.md` → `Doc/IMP-PLAN.md` → 단계별 구현 → 확인 → `uv tool install` → 다른 곳에서 사용 순서로 갑니다. VIBE-Q의 요구 표는 **내 SPEC을 쓴 뒤에** 비교하는 데 씁니다.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ uv run tools/flow.py ops --reset
|
||||||
|
✔ live run logs, added FAQ, created tickets, DLQ and alerts emptied, fault switch all ok
|
||||||
|
|
||||||
|
$ uv run tools/flow.py off my-log-report my-log-summary my-weekly my-slo-alert my-helpdesk
|
||||||
|
✔ unpublished my-log-report (id wfed832ca6c81732)
|
||||||
|
✔ unpublished my-weekly (id w269ccf31103289f)
|
||||||
|
✔ unpublished my-slo-alert (id w46a402ba838f33c)
|
||||||
|
✔ unpublished my-helpdesk (id wc5ae6e8bcdf8e39)
|
||||||
|
✔ n8n restarted. Turn it back on with: uv run tools/flow.py push <the workflow file>
|
||||||
|
|
||||||
|
$ uv run tools/flow.py push reference/baseline-report.json
|
||||||
|
✔ pushed '77 Log report (team baseline, provided)' (id w3f6d15ae763203b)
|
||||||
|
POST http://localhost:5678/webhook/base-report
|
||||||
|
|
||||||
|
$ 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
|
||||||
|
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||||
|
✔ run logs /logs (666 seeded runs) · FAQ additions /faq-extra (day 77 data API)
|
||||||
|
✔ n8n → http://data-api:8800
|
||||||
|
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||||
|
✔ node catalog (for check)
|
||||||
|
✔ CLI coding agent: codex, claude, agy
|
||||||
|
ready
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
$ uv run tools/flow.py logs --from 2026-10-19 --to 2026-10-30 --last 5
|
||||||
|
666 run(s) · errors 21 (3.2%)
|
||||||
|
route: 자동 답변 591 · 담당자 연결 34 · 오류 21 · 잠시 후 20
|
||||||
|
2026-10-30T17:05:38 success 자동 답변 1311ms 프린터 인쇄 대기열이 안 비워져요
|
||||||
|
2026-10-30T17:11:46 success 자동 답변 1175ms 메일 메일함 용량이 꽉 찼대요
|
||||||
|
2026-10-30T17:20:51 success 담당자 연결 79ms 와이파이 회의실 와이파이가 안 잡혀요
|
||||||
|
2026-10-30T17:24:39 success 자동 답변 1205ms 메일 첨부 파일이 너무 커서 안 보내져요
|
||||||
|
2026-10-30T17:59:20 success 자동 답변 1076ms 메일 메일함 용량이 꽉 찼대요
|
||||||
|
|
||||||
|
$ uv run tools/flow.py logs --from 2026-10-26 --to 2026-10-26 --last 0
|
||||||
|
70 run(s) · errors 16 (22.9%)
|
||||||
|
route: 자동 답변 50 · 오류 16 · 담당자 연결 4
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call base-report requests/two-weeks.json
|
||||||
|
HTTP 200 · 1.2s
|
||||||
|
{
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call base-report requests/monday.json
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01.json",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 전체",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 602 (expected 666)",
|
||||||
|
"error_rate = 3.488372093023256 (expected 0.0315)",
|
||||||
|
"p50_ms = null (expected 1205)",
|
||||||
|
"p95_ms = null (expected 2537)"
|
||||||
|
],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 갈래별",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_route.자동 답변 = 529 (expected 591)",
|
||||||
|
"by_route.담당자 연결 = 32 (expected 34)",
|
||||||
|
"by_route.오류 = null (expected 21)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 245 (expected 309)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 245,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"avg_ms": 1317.1551020408162,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 230,
|
||||||
|
"담당자 연결": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "둘째 주",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 293 (expected 357)",
|
||||||
|
"error_rate = 7.167235494880546 (expected 0.0588)",
|
||||||
|
"p95_ms = null (expected 2598)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 293,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 7.167235494880546,
|
||||||
|
"avg_ms": 1173.1399317406144,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 237,
|
||||||
|
"담당자 연결": 15,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "10월 26일 하루",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 0 (expected 70)",
|
||||||
|
"errors = 0 (expected 16)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01.json --webhook base-report -v
|
||||||
|
POST http://localhost:5678/webhook/base-report · 5 case(s) × 1
|
||||||
|
✘ 두 주 전체 · 0.2s · total = 602 (expected 666); error_rate = 3.488372093023256 (expected 0.0315); p50_ms = null (expected 1205); p95_ms = null (expected 2537)
|
||||||
|
→ request POST /webhook/base-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 두 주 · 갈래별 · 0.1s · by_route.자동 답변 = 529 (expected 591); by_route.담당자 연결 = 32 (expected 34); by_route.오류 = null (expected 21)
|
||||||
|
→ request POST /webhook/base-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 첫 주 · 0.1s · total = 245 (expected 309)
|
||||||
|
→ request POST /webhook/base-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 245,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"avg_ms": 1317.1551020408162,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 230,
|
||||||
|
"담당자 연결": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 둘째 주 · 0.1s · total = 293 (expected 357); error_rate = 7.167235494880546 (expected 0.0588); p95_ms = null (expected 2598)
|
||||||
|
→ request POST /webhook/base-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 293,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 7.167235494880546,
|
||||||
|
"avg_ms": 1173.1399317406144,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 237,
|
||||||
|
"담당자 연결": 15,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 10월 26일 하루 · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
|
||||||
|
→ request POST /webhook/base-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
Passed 0/5
|
||||||
|
report: runs/lab01-20261031-074859.json
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 전체",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 갈래별",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "둘째 주",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "10월 26일 하루",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab01-example2-report.json
|
||||||
|
✔ pushed '77 Log report (reference)' (id w1971efa15af33a8)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 전체 · 0.9s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 두 주 · 갈래별 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 첫 주 · 0.2s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 둘째 주 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 10월 26일 하루 · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-20261031-074912.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-log-report requests/week2.json
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-edges.json",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"passed": 2,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "하루 · from = to (포함)",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 0 (expected 70)",
|
||||||
|
"errors = 0 (expected 16)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "빈 기간 · 0건이어도 200",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"error_rate = null (expected 0)",
|
||||||
|
"p95_ms = null (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주말 · 실행이 없는 날",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-24",
|
||||||
|
"to": "2026-10-25"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": null,
|
||||||
|
"avg_ms": null,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "기간을 비우면 전체",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.153153153153153,
|
||||||
|
"avg_ms": 1259.3843843843845,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "source seed · 두 주 기록만",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 602 (expected 666)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"source": "seed"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 602,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.488372093023256,
|
||||||
|
"avg_ms": 1249.2392026578073,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 529,
|
||||||
|
"담당자 연결": 32,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "source live · 오늘 실습 기록 (아직 없음)",
|
||||||
|
"webhook": "base-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"total = 666 (expected 0)",
|
||||||
|
"error_rate = 3.153153153153153 (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 3.153153153153153,
|
||||||
|
"avg_ms": 1259.3843843843845,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01-edges.json -v
|
||||||
|
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/logs-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 하루 · from = to (포함) · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 빈 기간 · 0건이어도 200 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
✔ 주말 · 실행이 없는 날 · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-24",
|
||||||
|
"to": "2026-10-25"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
✔ 기간을 비우면 전체 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ source seed · 두 주 기록만 · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"source": "seed"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ source live · 오늘 실습 기록 (아직 없음) · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {}
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab01-edges-20261031-074913.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-edges.json --webhook base-report
|
||||||
|
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
POST http://localhost:5678/webhook/base-report · 6 case(s) × 1
|
||||||
|
✘ 하루 · from = to (포함) · 0.1s · total = 0 (expected 70); errors = 0 (expected 16)
|
||||||
|
✘ 빈 기간 · 0건이어도 200 · 0.1s · error_rate = null (expected 0); p95_ms = null (expected 0)
|
||||||
|
✔ 주말 · 실행이 없는 날 · 0.1s
|
||||||
|
✔ 기간을 비우면 전체 · 0.0s
|
||||||
|
✘ source seed · 두 주 기록만 · 0.1s · total = 602 (expected 666)
|
||||||
|
✘ source live · 오늘 실습 기록 (아직 없음) · 0.1s · total = 666 (expected 0); error_rate = 3.153153153153153 (expected 0)
|
||||||
|
Passed 2/6
|
||||||
|
report: runs/lab01-edges-20261031-074913.json
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "날짜별 · 월요일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_day.2026-10-26.total = null (expected 70)",
|
||||||
|
"by_day.2026-10-26.errors = null (expected 16)",
|
||||||
|
"by_day.2026-10-26.error_rate = null (expected 0.2286)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "날짜별 · 목요일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_day.2026-10-29.total = null (expected 97)",
|
||||||
|
"by_day.2026-10-29.errors = null (expected 5)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "날짜는 10일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_day has none of ['\"2026-10-19\"']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "릴리스별 다시 묻기 · 1.2.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_release has none of ['\"1.2.0\": {\"answers\": 8, \"repeats\": 5, \"repeat_rate\": 0.625}']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "릴리스별 다시 묻기 · 1.0.0 · 1.1.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_release has none of ['\"1.0.0\": {\"answers\": 402, \"repeats\": 19, \"repeat_rate\": 0.0473}']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "1.1.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"by_release has none of ['\"1.1.0\": {\"answers\": 181, \"repeats\": 14, \"repeat_rate\": 0.0773}']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✘ 날짜별 · 월요일 · 0.1s · by_day.2026-10-26.total = null (expected 70); by_day.2026-10-26.errors = null (expected 16); by_day.2026-10-26.error_rate = null (expected 0.2286)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 날짜별 · 목요일 · 0.1s · by_day.2026-10-29.total = null (expected 97); by_day.2026-10-29.errors = null (expected 5)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 날짜는 10일 · 0.1s · by_day has none of ['"2026-10-19"']
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 릴리스별 다시 묻기 · 1.2.0 · 0.1s · by_release has none of ['"1.2.0": {"answers": 8, "repeats": 5, "repeat_rate": 0.625}']
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s · by_release has none of ['"1.0.0": {"answers": 402, "repeats": 19, "repeat_rate": 0.0473}']
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 1.1.0 · 0.1s · by_release has none of ['"1.1.0": {"answers": 181, "repeats": 14, "repeat_rate": 0.0773}']
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 0/6
|
||||||
|
report: runs/lab01-challenge-20261031-074914.json
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 6,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "날짜별 · 월요일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "날짜별 · 목요일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "날짜는 10일",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "릴리스별 다시 묻기 · 1.2.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "릴리스별 다시 묻기 · 1.0.0 · 1.1.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "1.1.0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab01-challenge-day-release.json
|
||||||
|
! unpublished '77 Log report (reference)' (w1971efa15af33a8): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + days and releases (answer)' (id w5ee2fc1d2930303)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 날짜별 · 월요일 · 1.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 날짜별 · 목요일 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 날짜는 10일 · 0.2s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.2.0 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 1.1.0 · 0.0s
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab01-challenge-20261031-074930.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 전체 · 0.1s
|
||||||
|
✔ 두 주 · 갈래별 · 0.1s
|
||||||
|
✔ 첫 주 · 0.1s
|
||||||
|
✔ 둘째 주 · 0.1s
|
||||||
|
✔ 10월 26일 하루 · 0.1s
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-20261031-074930.json
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge2.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 · 단계별 p95",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"steps.faq_ms.p95 = null (expected 39)",
|
||||||
|
"steps.answer_ms.p95 = null (expected 2509)",
|
||||||
|
"steps.answer_ms.p50 = null (expected 1179)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 단계마다 잰 실행 수",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"steps.faq_ms.runs = null (expected 646)",
|
||||||
|
"steps.answer_ms.runs = null (expected 596)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "월요일 · FAQ 단계가 느림",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"steps.faq_ms.p95 = null (expected 1000)",
|
||||||
|
"steps.answer_ms.p95 = null (expected 2482)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 비용",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"cost_usd = null (expected 0.07836)",
|
||||||
|
"cost_per_run = null (expected 0.000118)",
|
||||||
|
"llm_calls = null (expected 596)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "빈 기간 · 비용 0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"cost_usd = null (expected 0)",
|
||||||
|
"cost_per_run = null (expected 0)",
|
||||||
|
"llm_calls = null (expected 0)",
|
||||||
|
"steps.faq_ms.p95 = null (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✘ 두 주 · 단계별 p95 · 0.1s · steps.faq_ms.p95 = null (expected 39); steps.answer_ms.p95 = null (expected 2509); steps.answer_ms.p50 = null (expected 1179)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 두 주 · 단계마다 잰 실행 수 · 0.1s · steps.faq_ms.runs = null (expected 646); steps.answer_ms.runs = null (expected 596)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 월요일 · FAQ 단계가 느림 · 0.1s · steps.faq_ms.p95 = null (expected 1000); steps.answer_ms.p95 = null (expected 2482)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 두 주 · 비용 · 0.1s · cost_usd = null (expected 0.07836); cost_per_run = null (expected 0.000118); llm_calls = null (expected 596)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 빈 기간 · 비용 0 · 0.0s · cost_usd = null (expected 0); cost_per_run = null (expected 0); llm_calls = null (expected 0); steps.faq_ms.p95 = null (expected 0)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {}
|
||||||
|
}
|
||||||
|
Passed 0/5
|
||||||
|
report: runs/lab01-challenge2-20261031-074930.json
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge2.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 · 단계별 p95",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 단계마다 잰 실행 수",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "월요일 · FAQ 단계가 느림",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 70,
|
||||||
|
"p50": 31,
|
||||||
|
"p95": 1000
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 50,
|
||||||
|
"p50": 1169,
|
||||||
|
"p95": 2482
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00645,
|
||||||
|
"cost_per_run": 9.2e-05,
|
||||||
|
"llm_calls": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 비용",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "빈 기간 · 비용 0",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab01-challenge2-steps-cost.json
|
||||||
|
! unpublished '77 Log report + days and releases (answer)' (w5ee2fc1d2930303): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + steps and cost (answer)' (id w0c1a020313bbffc)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 · 단계별 p95 · 0.9s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✔ 두 주 · 단계마다 잰 실행 수 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✔ 월요일 · FAQ 단계가 느림 · 0.2s · 50 calls · $0.006450
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 70,
|
||||||
|
"p50": 31,
|
||||||
|
"p95": 1000
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 50,
|
||||||
|
"p50": 1169,
|
||||||
|
"p95": 2482
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00645,
|
||||||
|
"cost_per_run": 9.2e-05,
|
||||||
|
"llm_calls": 50
|
||||||
|
}
|
||||||
|
✔ 두 주 · 비용 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✔ 빈 기간 · 비용 0 · 0.0s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-challenge2-20261031-074945.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 전체 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 갈래별 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 첫 주 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 둘째 주 · 0.1s · 304 calls · $0.039810
|
||||||
|
✔ 10월 26일 하루 · 0.0s · 50 calls · $0.006450
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-20261031-074945.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-challenge.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 날짜별 · 월요일 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 날짜별 · 목요일 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 날짜는 10일 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.2.0 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 릴리스별 다시 묻기 · 1.0.0 · 1.1.0 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 1.1.0 · 0.1s · 596 calls · $0.078360
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab01-challenge-20261031-074946.json
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 가장 많은 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"failures.0.node = null (expected Search FAQ)",
|
||||||
|
"failures.0.message = null (expected timeout of 1000ms exceeded)",
|
||||||
|
"failures.0.count = null (expected 16)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 두 번째 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"failures.1.node = null (expected Answer)",
|
||||||
|
"failures.1.count = null (expected 5)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 답하지 못한 주제",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"unanswered_topics.0.topic = null (expected 와이파이)",
|
||||||
|
"unanswered_topics.0.count = null (expected 34)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 1",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies.0.hour = null (expected 2026-10-26T09)",
|
||||||
|
"anomalies.0.total = null (expected 16)",
|
||||||
|
"anomalies.0.errors = null (expected 16)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 2",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies.1.hour = null (expected 2026-10-29T15)",
|
||||||
|
"anomalies.1.total = null (expected 7)",
|
||||||
|
"anomalies.1.errors = null (expected 5)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 첫 주에는 없음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies = null (expected [])"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✘ 예제 1 · 가장 많은 실패 · 0.1s · 596 calls · $0.078360 · failures.0.node = null (expected Search FAQ); failures.0.message = null (expected timeout of 1000ms exceeded); failures.0.count = null (expected 16)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✘ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360 · failures.1.node = null (expected Answer); failures.1.count = null (expected 5)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✘ 예제 1 · 답하지 못한 주제 · 0.1s · 596 calls · $0.078360 · unanswered_topics.0.topic = null (expected 와이파이); unanswered_topics.0.count = null (expected 34)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 이상 시간대 1 · 0.1s · 596 calls · $0.078360 · anomalies.0.hour = null (expected 2026-10-26T09); anomalies.0.total = null (expected 16); anomalies.0.errors = null (expected 16)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 이상 시간대 2 · 0.1s · 596 calls · $0.078360 · anomalies.1.hour = null (expected 2026-10-29T15); anomalies.1.total = null (expected 7); anomalies.1.errors = null (expected 5)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 첫 주에는 없음 · 0.0s · 292 calls · $0.038550 · anomalies = null (expected [])
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292
|
||||||
|
}
|
||||||
|
Passed 0/6
|
||||||
|
report: runs/lab02-20261031-074946.json
|
||||||
@@ -0,0 +1,738 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 3,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 가장 많은 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 두 번째 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 답하지 못한 주제",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 1",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies.0.hour = null (expected 2026-10-26T09)",
|
||||||
|
"anomalies.0.total = null (expected 16)",
|
||||||
|
"anomalies.0.errors = null (expected 16)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 2",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies.1.hour = null (expected 2026-10-29T15)",
|
||||||
|
"anomalies.1.total = null (expected 7)",
|
||||||
|
"anomalies.1.errors = null (expected 5)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 첫 주에는 없음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"anomalies = null (expected [])"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab02-example1-problems.json
|
||||||
|
! unpublished '77 Log report + steps and cost (answer)' (w0c1a020313bbffc): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + failures and unanswered topics (reference)' (id w51c62bb8e6f4e90)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 예제 1 · 가장 많은 실패 · 0.9s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 1 · 답하지 못한 주제 · 0.3s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 이상 시간대 1 · 0.1s · 596 calls · $0.078360 · anomalies.0.hour = null (expected 2026-10-26T09); anomalies.0.total = null (expected 16); anomalies.0.errors = null (expected 16)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 이상 시간대 2 · 0.1s · 596 calls · $0.078360 · anomalies.1.hour = null (expected 2026-10-29T15); anomalies.1.total = null (expected 7); anomalies.1.errors = null (expected 5)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 예제 2 · 첫 주에는 없음 · 0.1s · 292 calls · $0.038550 · anomalies = null (expected [])
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 3/6
|
||||||
|
report: runs/lab02-20261031-075001.json
|
||||||
@@ -0,0 +1,799 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 6,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 가장 많은 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 두 번째 실패",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 1 · 답하지 못한 주제",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 1",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 이상 시간대 2",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "예제 2 · 첫 주에는 없음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,906 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab02-example2-anomalies.json
|
||||||
|
! unpublished '77 Log report + failures and unanswered topics (reference)' (w51c62bb8e6f4e90): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + anomalies (reference)' (id w4957bb6724af44d)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 예제 1 · 가장 많은 실패 · 0.9s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 1 · 답하지 못한 주제 · 0.2s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 2 · 이상 시간대 1 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 2 · 이상 시간대 2 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 예제 2 · 첫 주에는 없음 · 0.0s · 292 calls · $0.038550
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-20261031-075015.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-challenge2.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 · 단계별 p95 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 단계마다 잰 실행 수 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 월요일 · FAQ 단계가 느림 · 0.0s · 50 calls · $0.006450
|
||||||
|
✔ 두 주 · 비용 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 빈 기간 · 비용 0 · 0.0s · 0 calls · $0.000000
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-challenge2-20261031-075015.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-log-report requests/two-weeks.json
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-periods.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "첫 주 · 오류 0 → 이상 없음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "둘째 주 · 기준 0.0588 × 3",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "월요일만 · 기준 0.2286 × 3",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 70,
|
||||||
|
"p50": 31,
|
||||||
|
"p95": 1000
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 50,
|
||||||
|
"p50": 1169,
|
||||||
|
"p95": 2482
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00645,
|
||||||
|
"cost_per_run": 9.2e-05,
|
||||||
|
"llm_calls": 50,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "목요일만 · 15시만",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-29",
|
||||||
|
"to": "2026-10-29"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515,
|
||||||
|
"p50_ms": 1150,
|
||||||
|
"p95_ms": 2533,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 66,
|
||||||
|
"담당자 연결": 6,
|
||||||
|
"잠시 후": 20,
|
||||||
|
"오류": 5
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 66,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0758
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 77,
|
||||||
|
"p50": 24,
|
||||||
|
"p95": 38
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 71,
|
||||||
|
"p50": 1100,
|
||||||
|
"p95": 2539
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00942,
|
||||||
|
"cost_per_run": 9.7e-05,
|
||||||
|
"llm_calls": 71,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 기준 0.0315 × 3",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-periods.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 첫 주 · 오류 0 → 이상 없음 · 0.1s · 292 calls · $0.038550
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
✔ 둘째 주 · 기준 0.0588 × 3 · 0.1s · 304 calls · $0.039810
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 월요일만 · 기준 0.2286 × 3 · 0.0s · 50 calls · $0.006450
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-26"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"p50_ms": 1082,
|
||||||
|
"p95_ms": 2468,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 16,
|
||||||
|
"자동 답변": 50,
|
||||||
|
"담당자 연결": 4
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 50,
|
||||||
|
"repeats": 1,
|
||||||
|
"repeat_rate": 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 70,
|
||||||
|
"p50": 31,
|
||||||
|
"p95": 1000
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 50,
|
||||||
|
"p50": 1169,
|
||||||
|
"p95": 2482
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00645,
|
||||||
|
"cost_per_run": 9.2e-05,
|
||||||
|
"llm_calls": 50,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 목요일만 · 15시만 · 0.1s · 71 calls · $0.009420
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-29",
|
||||||
|
"to": "2026-10-29"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515,
|
||||||
|
"p50_ms": 1150,
|
||||||
|
"p95_ms": 2533,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 66,
|
||||||
|
"담당자 연결": 6,
|
||||||
|
"잠시 후": 20,
|
||||||
|
"오류": 5
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 66,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0758
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 77,
|
||||||
|
"p50": 24,
|
||||||
|
"p95": 38
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 71,
|
||||||
|
"p50": 1100,
|
||||||
|
"p95": 2539
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.00942,
|
||||||
|
"cost_per_run": 9.7e-05,
|
||||||
|
"llm_calls": 71,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 두 주 · 기준 0.0315 × 3 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab02-periods-20261031-075016.json
|
||||||
@@ -0,0 +1,671 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 1,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 · 다시 묻는 비율이 가장 높은 주제",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"repeat_topics.0.topic = null (expected 프린터)",
|
||||||
|
"repeat_topics.0.answers = null (expected 81)",
|
||||||
|
"repeat_topics.0.repeats = null (expected 8)",
|
||||||
|
"repeat_topics.0.repeat_rate = null (expected 0.0988)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"repeat_topics.1.topic = null (expected 비밀번호)",
|
||||||
|
"repeat_topics.1.repeats = null (expected 9)",
|
||||||
|
"repeat_topics.1.repeat_rate = null (expected 0.0647)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 다시 물은 주제는 6개",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"repeat_topics.5.topic = null (expected 설치)",
|
||||||
|
"repeat_topics.5.repeat_rate = null (expected 0.0328)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주 · 설치는 다시 묻지 않음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"repeat_topics.0.topic = null (expected 프린터)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "앞의 지표는 그대로",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✘ 두 주 · 다시 묻는 비율이 가장 높은 주제 · 0.1s · 596 calls · $0.078360 · repeat_topics.0.topic = null (expected 프린터); repeat_topics.0.answers = null (expected 81); repeat_topics.0.repeats = null (expected 8); repeat_topics.0.repeat_rate = null (expected 0.0988)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위 · 0.1s · 596 calls · $0.078360 · repeat_topics.1.topic = null (expected 비밀번호); repeat_topics.1.repeats = null (expected 9); repeat_topics.1.repeat_rate = null (expected 0.0647)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 두 주 · 다시 물은 주제는 6개 · 0.1s · 596 calls · $0.078360 · repeat_topics.5.topic = null (expected 설치); repeat_topics.5.repeat_rate = null (expected 0.0328)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 첫 주 · 설치는 다시 묻지 않음 · 0.0s · 292 calls · $0.038550 · repeat_topics.0.topic = null (expected 프린터)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
✔ 앞의 지표는 그대로 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 1/5
|
||||||
|
report: runs/lab02-challenge-20261031-075016.json
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "두 주 · 다시 묻는 비율이 가장 높은 주제",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "두 주 · 다시 물은 주제는 6개",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주 · 설치는 다시 묻지 않음",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "앞의 지표는 그대로",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab02-challenge-repeat-topics.json
|
||||||
|
! unpublished '77 Log report + anomalies (reference)' (w4957bb6724af44d): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + repeat topics (answer)' (id wf3d06e3d30eeee6)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 · 다시 묻는 비율이 가장 높은 주제 · 0.9s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위 · 0.3s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 두 주 · 다시 물은 주제는 6개 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 첫 주 · 설치는 다시 묻지 않음 · 0.0s · 292 calls · $0.038550
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 앞의 지표는 그대로 · 0.1s · 596 calls · $0.078360
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 591,
|
||||||
|
"담당자 연결": 34,
|
||||||
|
"오류": 21,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 402,
|
||||||
|
"repeats": 19,
|
||||||
|
"repeat_rate": 0.0473
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 646,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 596,
|
||||||
|
"p50": 1179,
|
||||||
|
"p95": 2509
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 81,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 139,
|
||||||
|
"repeats": 9,
|
||||||
|
"repeat_rate": 0.0647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 124,
|
||||||
|
"repeats": 8,
|
||||||
|
"repeat_rate": 0.0645
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 96,
|
||||||
|
"repeats": 6,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 90,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0556
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 61,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0328
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab02-challenge-20261031-075031.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 6 case(s) × 1
|
||||||
|
✔ 예제 1 · 가장 많은 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 두 번째 실패 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 1 · 답하지 못한 주제 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 이상 시간대 1 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 이상 시간대 2 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 예제 2 · 첫 주에는 없음 · 0.0s · 292 calls · $0.038550
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-20261031-075031.json
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge2.json",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 3,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "요약 · 숫자는 통계에 있는 것만",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"numbers_ok = null (expected true)",
|
||||||
|
"top_topic = null (expected 와이파이)",
|
||||||
|
"worst_hour = null (expected 2026-10-26T09)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "요약 · 주제와 시간대를 말함",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"summary has none of ['와이파이']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "요약 · 날짜",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"summary has none of ['10월 26일', '10-26', '26일']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-summary · 3 case(s) × 1
|
||||||
|
✘ 요약 · 숫자는 통계에 있는 것만 · 0.0s · status 404 (expected 200); numbers_ok = null (expected true); top_topic = null (expected 와이파이); worst_hour = null (expected 2026-10-26T09)
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 요약 · 주제와 시간대를 말함 · 0.0s · status 404 (expected 200); summary has none of ['와이파이']
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 요약 · 날짜 · 0.0s · status 404 (expected 200); summary has none of ['10월 26일', '10-26', '26일']
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-log-summary\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
Passed 0/3
|
||||||
|
report: runs/lab02-challenge2-20261031-075032.json
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge2.json",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"passed": 3,
|
||||||
|
"total": 3,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "요약 · 숫자는 통계에 있는 것만",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 3.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 현상은 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "요약 · 주제와 시간대를 말함",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.6,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "요약 · 날짜",
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.7,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건이 보고되었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab02-challenge2-summary.json
|
||||||
|
✔ pushed '77 Log summary (answer)' (id w9c86d93226f0b0f)
|
||||||
|
POST http://localhost:5678/webhook/my-log-summary
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-summary · 3 case(s) × 1
|
||||||
|
✔ 요약 · 숫자는 통계에 있는 것만 · 3.4s
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 현상은 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
✔ 요약 · 주제와 시간대를 말함 · 2.6s
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
✔ 요약 · 날짜 · 1.7s
|
||||||
|
→ request POST /webhook/my-log-summary
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 34건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건이 보고되었습니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
Passed 3/3
|
||||||
|
report: runs/lab02-challenge2-20261031-075051.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-log-summary requests/week2.json
|
||||||
|
HTTP 200 · 1.8s
|
||||||
|
{
|
||||||
|
"summary": "가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.",
|
||||||
|
"numbers_ok": true,
|
||||||
|
"unknown_numbers": [],
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"cases": "replay-before.json",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"passed": 10,
|
||||||
|
"total": 10,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "before · 회의실 와이파이가 안 ",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 사내 Wi-Fi 비밀번",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 와이파이가 자주 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 노트북이 와이파이에 연",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 손님용 wifi도 있나",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · VPN이 자꾸 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 비밀번호를 잊어버렸어요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 프린터 출력이 안 돼요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 메일함 용량이 꽉 찼대",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "before · 새 프로그램 설치는 어",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
$ uv run tools/flow.py push reference/helpdesk-live.json
|
||||||
|
✔ pushed '77 Live helpdesk (provided)' (id wb754efbe6b67af7)
|
||||||
|
POST http://localhost:5678/webhook/my-helpdesk
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/replay-before.json -v
|
||||||
|
✔ before: POST http://localhost:8800/logs-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/logs-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
|
||||||
|
→ request POST http://localhost:8800/admin/fault
|
||||||
|
{
|
||||||
|
"faq": "slow",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"faq": "slow",
|
||||||
|
"tickets": "ok",
|
||||||
|
"prompts": "ok",
|
||||||
|
"usage": "ok",
|
||||||
|
"dlq": "ok",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-helpdesk · 10 case(s) × 1
|
||||||
|
✔ before · 회의실 와이파이가 안 · 2.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 사내 Wi-Fi 비밀번 · 1.2s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 와이파이가 자주 끊겨요 · 1.2s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 노트북이 와이파이에 연 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 손님용 wifi도 있나 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · VPN이 자꾸 끊겨요 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 비밀번호를 잊어버렸어요 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 프린터 출력이 안 돼요 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 메일함 용량이 꽉 찼대 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
✔ before · 새 프로그램 설치는 어 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "before"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "담당자 연결",
|
||||||
|
"status": "error",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "지금은 답할 수 없어 담당자에게 연결합니다.",
|
||||||
|
"error": "timeout of 1000ms exceeded"
|
||||||
|
}
|
||||||
|
Passed 10/10
|
||||||
|
report: runs/replay-before-20261031-075117.json
|
||||||
|
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
|
||||||
|
→ request POST http://localhost:8800/admin/fault
|
||||||
|
{
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok",
|
||||||
|
"prompts": "ok",
|
||||||
|
"usage": "ok",
|
||||||
|
"dlq": "ok",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py logs --tag before --source live --last 3
|
||||||
|
10 run(s) · errors 10 (100.0%)
|
||||||
|
route: 오류 10
|
||||||
|
2026-10-31T07:51:15 error None 1034ms 프린터 Search FAQ 프린터 출력이 안 돼요
|
||||||
|
2026-10-31T07:51:16 error None 1035ms 메일 Search FAQ 메일함 용량이 꽉 찼대요
|
||||||
|
2026-10-31T07:51:17 error None 1032ms 설치 Search FAQ 새 프로그램 설치는 어떻게 신청하나요?
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✔ 고치기 전 · 10건 모두 실패 · 0.1s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1,
|
||||||
|
"p50_ms": 1043,
|
||||||
|
"p95_ms": 1098,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1043,
|
||||||
|
"p95": 1098
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✔ 고치기 전 · FAQ timeout · 0.1s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1,
|
||||||
|
"p50_ms": 1043,
|
||||||
|
"p95_ms": 1098,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1043,
|
||||||
|
"p95": 1098
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✘ 고친 뒤 · 오류 없음 · 0.1s · 0 calls · $0.000000 · total = 0 (expected 10); by_route.자동 답변 = null (expected 10)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 답하지 못한 주제 없음 · 0.0s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 0,
|
||||||
|
"p95_ms": 0,
|
||||||
|
"by_route": {},
|
||||||
|
"by_day": {},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
Passed 3/4
|
||||||
|
report: runs/lab03-20261031-075118.json
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"cases": "replay-after.json",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"passed": 10,
|
||||||
|
"total": 10,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "after · 회의실 와이파이가 안 ",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 4.5,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 사내 Wi-Fi 비밀번",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 비밀번호는 별도로 없으며, 사번으로 인증합니다. 회의실에서 연결이 안 되면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 와이파이가 자주 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실 등 특정 장소에서 와이파이가 자주 끊기면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 노트북이 와이파이에 연",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 손님용 wifi도 있나",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "손님용 와이파이에 대한 안내는 제공되지 않습니다. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인하여 사용합니다. 추가 문의 사항은 IT 포털에 문의해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · VPN이 자꾸 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 비밀번호를 잊어버렸어요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 프린터 출력이 안 돼요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 메일함 용량이 꽉 찼대",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.6,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "after · 새 프로그램 설치는 어",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "새 프로그램 설치는 IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab03-example1-helpdesk-fixed.json
|
||||||
|
! unpublished '77 Live helpdesk (provided)' (wb754efbe6b67af7): it used the same webhook path
|
||||||
|
✔ pushed '77 Live helpdesk fixed (reference)' (id wc5ae6e8bcdf8e39)
|
||||||
|
POST http://localhost:5678/webhook/my-helpdesk
|
||||||
|
|
||||||
|
$ curl -s -XPOST localhost:8800/faq/add -H 'Content-Type: application/json' -d @requests/faq-wifi.json
|
||||||
|
{"added": "FAQ-007", "size": 7}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/replay-after.json -v
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "slow", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
|
||||||
|
→ request POST http://localhost:8800/admin/fault
|
||||||
|
{
|
||||||
|
"faq": "slow",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"faq": "slow",
|
||||||
|
"tickets": "ok",
|
||||||
|
"prompts": "ok",
|
||||||
|
"usage": "ok",
|
||||||
|
"dlq": "ok",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-helpdesk · 10 case(s) × 1
|
||||||
|
✔ after · 회의실 와이파이가 안 · 4.5s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 사내 Wi-Fi 비밀번 · 2.4s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 비밀번호는 별도로 없으며, 사번으로 인증합니다. 회의실에서 연결이 안 되면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 와이파이가 자주 끊겨요 · 2.4s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실 등 특정 장소에서 와이파이가 자주 끊기면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 노트북이 와이파이에 연 · 2.3s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 손님용 wifi도 있나 · 2.9s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "손님용 와이파이에 대한 안내는 제공되지 않습니다. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인하여 사용합니다. 추가 문의 사항은 IT 포털에 문의해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · VPN이 자꾸 끊겨요 · 2.4s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 비밀번호를 잊어버렸어요 · 2.3s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 프린터 출력이 안 돼요 · 2.3s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 메일함 용량이 꽉 찼대 · 2.6s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ after · 새 프로그램 설치는 어 · 2.4s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "after"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "새 프로그램 설치는 IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
Passed 10/10
|
||||||
|
report: runs/replay-after-20261031-075158.json
|
||||||
|
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
|
||||||
|
→ request POST http://localhost:8800/admin/fault
|
||||||
|
{
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok",
|
||||||
|
"prompts": "ok",
|
||||||
|
"usage": "ok",
|
||||||
|
"dlq": "ok",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✔ 고치기 전 · 10건 모두 실패 · 0.1s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1,
|
||||||
|
"p50_ms": 1043,
|
||||||
|
"p95_ms": 1098,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1043,
|
||||||
|
"p95": 1098
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✔ 고치기 전 · FAQ timeout · 0.1s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1,
|
||||||
|
"p50_ms": 1043,
|
||||||
|
"p95_ms": 1098,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1043,
|
||||||
|
"p95": 1098
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 오류 없음 · 0.1s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 답하지 못한 주제 없음 · 0.0s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-20261031-075159.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py ops
|
||||||
|
fault switch: all ok
|
||||||
|
live run logs: 20 (tag before 10 · tag after 10)
|
||||||
|
added FAQ: 1 (FAQ-007)
|
||||||
|
created tickets: 0
|
||||||
|
DLQ: 0 (parked 0)
|
||||||
|
alerts: 0
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-slo.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "둘째 주 · SLO 못 지킴",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"slo.ok = null (expected false)",
|
||||||
|
"unanswered_rate = null (expected 0.0476)",
|
||||||
|
"slo has none of ['\"error_rate\"', '\"unanswered_rate\"']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 36,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.1111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 24,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0833
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 70,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 56,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 48,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 65,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0615
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주 · 답 못 함만",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"slo.ok = null (expected false)",
|
||||||
|
"slo.breaches = null (expected [\"unanswered_rate\"])"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "기준을 바꾸면 · 첫 주",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"slo.ok = null (expected true)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "고친 뒤 · 오류 · 답 못 함 기준은 지킴",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"unanswered_rate = null (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-slo.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✘ 둘째 주 · SLO 못 지킴 · 0.1s · 304 calls · $0.039810 · slo.ok = null (expected false); unanswered_rate = null (expected 0.0476); slo has none of ['"error_rate"', '"unanswered_rate"']
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 36,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.1111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 24,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0833
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 70,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 56,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 48,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 65,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0615
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 첫 주 · 답 못 함만 · 0.1s · 292 calls · $0.038550 · slo.ok = null (expected false); slo.breaches = null (expected ["unanswered_rate"])
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 기준을 바꾸면 · 첫 주 · 0.1s · 292 calls · $0.038550 · slo.ok = null (expected true)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 고친 뒤 · 오류 · 답 못 함 기준은 지킴 · 0.1s · 10 calls · $0.000000 · unanswered_rate = null (expected 0)
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": []
|
||||||
|
}
|
||||||
|
Passed 0/4
|
||||||
|
report: runs/lab03-slo-20261031-075159.json
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-slo.json",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "둘째 주 · SLO 못 지킴",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 36,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.1111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 24,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0833
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 70,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 56,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 48,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 65,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0615
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.0476,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "첫 주 · 답 못 함만",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.055,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "기준을 바꾸면 · 첫 주",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.055,
|
||||||
|
"slo": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "고친 뒤 · 오류 · 답 못 함 기준은 지킴",
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"p95_ms"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,540 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab03-example2-slo.json
|
||||||
|
! unpublished '77 Log report + repeat topics (answer)' (wf3d06e3d30eeee6): it used the same webhook path
|
||||||
|
✔ pushed '77 Log report + SLO (reference)' (id wfed832ca6c81732)
|
||||||
|
POST http://localhost:5678/webhook/my-log-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-slo.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✔ 둘째 주 · SLO 못 지킴 · 0.9s · 304 calls · $0.039810
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p50_ms": 1171,
|
||||||
|
"p95_ms": 2598,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 21,
|
||||||
|
"자동 답변": 299,
|
||||||
|
"담당자 연결": 17,
|
||||||
|
"잠시 후": 20
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-26": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 0.2286
|
||||||
|
},
|
||||||
|
"2026-10-27": {
|
||||||
|
"total": 62,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-28": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-29": {
|
||||||
|
"total": 97,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.0515
|
||||||
|
},
|
||||||
|
"2026-10-30": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 110,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0273
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 181,
|
||||||
|
"repeats": 14,
|
||||||
|
"repeat_rate": 0.0773
|
||||||
|
},
|
||||||
|
"1.2.0": {
|
||||||
|
"answers": 8,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 337,
|
||||||
|
"p50": 28,
|
||||||
|
"p95": 40
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 304,
|
||||||
|
"p50": 1162,
|
||||||
|
"p95": 2532
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03981,
|
||||||
|
"cost_per_run": 0.000112,
|
||||||
|
"llm_calls": 304,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Answer",
|
||||||
|
"message": "429 Rate limit reached for requests",
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [
|
||||||
|
{
|
||||||
|
"hour": "2026-10-26T09",
|
||||||
|
"total": 16,
|
||||||
|
"errors": 16,
|
||||||
|
"error_rate": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "2026-10-29T15",
|
||||||
|
"total": 7,
|
||||||
|
"errors": 5,
|
||||||
|
"error_rate": 0.7143
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 36,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.1111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "설치",
|
||||||
|
"answers": 24,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0833
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 70,
|
||||||
|
"repeats": 5,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 56,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 48,
|
||||||
|
"repeats": 3,
|
||||||
|
"repeat_rate": 0.0625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 65,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0615
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.0476,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 첫 주 · 답 못 함만 · 0.1s · 292 calls · $0.038550
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.055,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 기준을 바꾸면 · 첫 주 · 0.1s · 292 calls · $0.038550
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1252,
|
||||||
|
"p95_ms": 2525,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 292,
|
||||||
|
"담당자 연결": 17
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-19": {
|
||||||
|
"total": 73,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-20": {
|
||||||
|
"total": 57,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-21": {
|
||||||
|
"total": 60,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-22": {
|
||||||
|
"total": 55,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
},
|
||||||
|
"2026-10-23": {
|
||||||
|
"total": 64,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.0.0": {
|
||||||
|
"answers": 292,
|
||||||
|
"repeats": 16,
|
||||||
|
"repeat_rate": 0.0548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 309,
|
||||||
|
"p50": 27,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 292,
|
||||||
|
"p50": 1200,
|
||||||
|
"p95": 2470
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0.03855,
|
||||||
|
"cost_per_run": 0.000125,
|
||||||
|
"llm_calls": 292,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [
|
||||||
|
{
|
||||||
|
"topic": "와이파이",
|
||||||
|
"count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [
|
||||||
|
{
|
||||||
|
"topic": "프린터",
|
||||||
|
"answers": 45,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "VPN",
|
||||||
|
"answers": 59,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.0678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answers": 69,
|
||||||
|
"repeats": 4,
|
||||||
|
"repeat_rate": 0.058
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "접수 현황",
|
||||||
|
"answers": 40,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "메일",
|
||||||
|
"answers": 42,
|
||||||
|
"repeats": 2,
|
||||||
|
"repeat_rate": 0.0476
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_rate": 0.055,
|
||||||
|
"slo": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 오류 · 답 못 함 기준은 지킴 · 0.2s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"p95_ms"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-slo-20261031-075213.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-log-report '{"tag": "after", "source": "live"}'
|
||||||
|
HTTP 200 · 0.0s
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"p95_ms"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 5 case(s) × 1
|
||||||
|
✔ 두 주 · 다시 묻는 비율이 가장 높은 주제 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 두 주 · 다시 물은 주제는 6개 · 0.1s · 596 calls · $0.078360
|
||||||
|
✔ 첫 주 · 설치는 다시 묻지 않음 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 앞의 지표는 그대로 · 0.1s · 596 calls · $0.078360
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab02-challenge-20261031-075213.json
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"cases": "replay-normal.json",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"passed": 10,
|
||||||
|
"total": 10,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "normal · 회의실 와이파이가 안 ",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 사내 Wi-Fi 비밀번",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 비밀번호는 별도로 없으며, 사번으로 인증합니다. 회의실에서 연결이 안 되면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 와이파이가 자주 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실 등 특정 장소에서 와이파이가 자주 끊기면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 노트북이 와이파이에 연",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 손님용 wifi도 있나",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "손님용 와이파이에 대한 안내는 제공되지 않습니다. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인하여 사용합니다. 추가 문의 사항은 IT 포털에 문의해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · VPN이 자꾸 끊겨요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 비밀번호를 잊어버렸어요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.8,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 프린터 출력이 안 돼요",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 메일함 용량이 꽉 찼대",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "normal · 새 프로그램 설치는 어",
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "새 프로그램 설치는 IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
$ uv run tools/flow.py test cases/replay-normal.json -v
|
||||||
|
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
|
||||||
|
→ request POST http://localhost:8800/admin/fault
|
||||||
|
{
|
||||||
|
"faq": "ok"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok",
|
||||||
|
"prompts": "ok",
|
||||||
|
"usage": "ok",
|
||||||
|
"dlq": "ok",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-helpdesk · 10 case(s) × 1
|
||||||
|
✔ normal · 회의실 와이파이가 안 · 2.3s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 사내 Wi-Fi 비밀번 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 비밀번호는 별도로 없으며, 사번으로 인증합니다. 회의실에서 연결이 안 되면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 와이파이가 자주 끊겨요 · 1.2s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실 등 특정 장소에서 와이파이가 자주 끊기면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 노트북이 와이파이에 연 · 0.9s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 손님용 wifi도 있나 · 1.2s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "와이파이",
|
||||||
|
"answer": "손님용 와이파이에 대한 안내는 제공되지 않습니다. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인하여 사용합니다. 추가 문의 사항은 IT 포털에 문의해 주세요.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · VPN이 자꾸 끊겨요 · 0.9s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "VPN",
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 비밀번호를 잊어버렸어요 · 0.8s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "비밀번호",
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 프린터 출력이 안 돼요 · 1.2s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "프린터",
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 메일함 용량이 꽉 찼대 · 1.0s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "메일",
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
✔ normal · 새 프로그램 설치는 어 · 1.1s
|
||||||
|
→ request POST /webhook/my-helpdesk
|
||||||
|
{
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "normal"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success",
|
||||||
|
"topic": "설치",
|
||||||
|
"answer": "새 프로그램 설치는 IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
Passed 10/10
|
||||||
|
report: runs/replay-normal-20261031-075225.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-cost.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✔ 고치기 전 · 느린 FAQ · 1초에 끊김 · 0.1s · 0 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1,
|
||||||
|
"p50_ms": 1043,
|
||||||
|
"p95_ms": 1098,
|
||||||
|
"by_route": {
|
||||||
|
"오류": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10,
|
||||||
|
"error_rate": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1043,
|
||||||
|
"p95": 1098
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 0,
|
||||||
|
"p50": 0,
|
||||||
|
"p95": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"failures": [
|
||||||
|
{
|
||||||
|
"node": "Search FAQ",
|
||||||
|
"message": "timeout of 1000ms exceeded",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 느린 FAQ · 기다려서 답함 · 0.1s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 2342,
|
||||||
|
"p95_ms": 3560,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1238,
|
||||||
|
"p95": 1278
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 1103,
|
||||||
|
"p95": 2282
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"p95_ms"
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 평소 FAQ · 기다릴 일이 없음 · 0.0s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "normal",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1015,
|
||||||
|
"p95_ms": 2147,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 17,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 997,
|
||||||
|
"p95": 2108
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 고친 뒤 · 답 단계는 FAQ 속도와 상관없음 · 0.1s · 10 calls · $0.000000
|
||||||
|
→ request POST /webhook/my-log-report
|
||||||
|
{
|
||||||
|
"tag": "normal",
|
||||||
|
"source": "live"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p50_ms": 1015,
|
||||||
|
"p95_ms": 2147,
|
||||||
|
"by_route": {
|
||||||
|
"자동 답변": 10
|
||||||
|
},
|
||||||
|
"by_day": {
|
||||||
|
"2026-10-31": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_release": {
|
||||||
|
"1.1.0": {
|
||||||
|
"answers": 10,
|
||||||
|
"repeats": 0,
|
||||||
|
"repeat_rate": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"faq_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 17,
|
||||||
|
"p95": 39
|
||||||
|
},
|
||||||
|
"answer_ms": {
|
||||||
|
"runs": 10,
|
||||||
|
"p50": 997,
|
||||||
|
"p95": 2108
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 10,
|
||||||
|
"failures": [],
|
||||||
|
"unanswered_topics": [],
|
||||||
|
"anomalies": [],
|
||||||
|
"repeat_topics": [],
|
||||||
|
"unanswered_rate": 0,
|
||||||
|
"slo": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"limits": {
|
||||||
|
"error_rate": 0.01,
|
||||||
|
"p95_ms": 3000,
|
||||||
|
"unanswered_rate": 0.02
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-cost-20261031-075226.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py ops
|
||||||
|
fault switch: all ok
|
||||||
|
live run logs: 30 (tag before 10 · tag after 10 · tag normal 10)
|
||||||
|
added FAQ: 1 (FAQ-007)
|
||||||
|
created tickets: 0
|
||||||
|
DLQ: 0 (parked 0)
|
||||||
|
alerts: 0
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge.json",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 7,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 숫자",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"total = null (expected 357)",
|
||||||
|
"error_rate = null (expected 0.0588)",
|
||||||
|
"slo_ok = null (expected false)",
|
||||||
|
"numbers_ok = null (expected true)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 절",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of ['## 숫자']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 문제 절",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of ['## 문제']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 다음 할 일",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of ['## 다음 할 일']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 비교 숫자",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of ['| 요청 | 309 | 357 |']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 이상 시간대",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of ['2026-10-26 09시']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 와이파이",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"markdown has none of [\"FAQ에 '와이파이' 항목 추가\"]"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-weekly · 7 case(s) × 1
|
||||||
|
✘ 주간 보고 · 숫자 · 0.0s · status 404 (expected 200); total = null (expected 357); error_rate = null (expected 0.0588); slo_ok = null (expected false); numbers_ok = null (expected true)
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 절 · 0.0s · status 404 (expected 200); markdown has none of ['## 숫자']
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 문제 절 · 0.0s · status 404 (expected 200); markdown has none of ['## 문제']
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 다음 할 일 · 0.0s · status 404 (expected 200); markdown has none of ['## 다음 할 일']
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 비교 숫자 · 0.0s · status 404 (expected 200); markdown has none of ['| 요청 | 309 | 357 |']
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 이상 시간대 · 0.0s · status 404 (expected 200); markdown has none of ['2026-10-26 09시']
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 주간 보고 · 와이파이 · 0.0s · status 404 (expected 200); markdown has none of ["FAQ에 '와이파이' 항목 추가"]
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-weekly\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
Passed 0/7
|
||||||
|
report: runs/lab03-challenge-20261031-075226.json
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge.json",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"passed": 7,
|
||||||
|
"total": 7,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 숫자",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 3.6,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 절",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 문제 절",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 다음 할 일",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.7,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 비교 숫자",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 이상 시간대",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.8,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "주간 보고 · 와이파이",
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.7,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab03-challenge-weekly.json
|
||||||
|
✔ pushed '77 Weekly report (answer)' (id w269ccf31103289f)
|
||||||
|
POST http://localhost:5678/webhook/my-weekly
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-weekly · 7 case(s) × 1
|
||||||
|
✔ 주간 보고 · 숫자 · 3.6s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 절 · 1.9s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 문제 절 · 2.4s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 다음 할 일 · 1.7s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 징후는 10월 26일 09시에 발생했으며, 오류 16건과 총 16건의 요청이 있었습니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 비교 숫자 · 2.0s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 이상 시간대 · 1.8s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
✔ 주간 보고 · 와이파이 · 1.7s
|
||||||
|
→ request POST /webhook/my-weekly
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"prev_from": "2026-10-19",
|
||||||
|
"prev_to": "2026-10-23"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 이상 징후가 가장 심한 시간대는 10월 26일 09시로, 총 16건 중 16건이 오류입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
Passed 7/7
|
||||||
|
report: runs/lab03-challenge-20261031-075252.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-weekly requests/weekly.json
|
||||||
|
HTTP 200 · 1.9s
|
||||||
|
{
|
||||||
|
"markdown": "# IT 헬프데스크 주간 운영 보고 (2026-10-26 ~ 2026-10-30)\n\n## 숫자\n\n| 지표 | 지난주 | 이번 주 |\n| --- | --- | --- |\n| 요청 | 309 | 357 |\n| 오류율 | 0% | 5.88% |\n| p95 | 2525ms | 2598ms |\n| 답 못 함 | 5.5% | 4.76% |\n| SLO | 못 지킴 unanswered_rate | 못 지킴 error_rate, unanswered_rate |\n\n## 문제\n\n- 실패 16건 · Search FAQ · timeout of 1000ms exceeded\n- 실패 5건 · Answer · 429 Rate limit reached for requests\n- 답하지 못한 질문 17건 · 와이파이\n- 이상 시간대 2026-10-26 09시 · 16/16 실패\n- 이상 시간대 2026-10-29 15시 · 5/7 실패\n\n## 요약 (LLM, 숫자 확인 통과)\n\n가장 많은 미응답 주제는 와이파이로 17건입니다. 가장 흔한 장애는 Search FAQ 노드에서 발생한 \"timeout of 1000ms exceeded\" 메시지로 16건입니다. 가장 심각한 이상 시간대는 10월 26일 09시로 오류 16건, 전체 16건입니다.\n\n## 다음 할 일\n\n- FAQ에 '와이파이' 항목 추가\n- FAQ 검색 timeout · 재시도 설정 점검 (72일차)\n- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)",
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge2.json",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "둘째 주 · SLO 못 지킴 → 알림",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"ok = null (expected false)",
|
||||||
|
"breaches = null (expected [\"error_rate\", \"unanswered_rate\"])",
|
||||||
|
"alert = null (expected sent)",
|
||||||
|
"message has none of ['error_rate 0.0588 > 0.01']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림 확인 (high · slo)",
|
||||||
|
"webhook": "http://localhost:8800/alerts/latest",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"workflow = null (expected slo)",
|
||||||
|
"severity = null (expected high)",
|
||||||
|
"reason = null (expected slo)",
|
||||||
|
"message has none of ['unanswered_rate']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "10분 안에 다시 확인 → 보내지 않음",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"ok = null (expected false)",
|
||||||
|
"alert = null (expected suppressed)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "slo 알림은 1개",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 0 (expected 1)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "기준 안이면 알림 없음",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"ok = null (expected true)",
|
||||||
|
"breaches = null (expected [])",
|
||||||
|
"alert = null (expected none)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "그래도 알림은 1개",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 0 (expected 1)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-slo-alert · 6 case(s) × 1
|
||||||
|
✘ 둘째 주 · SLO 못 지킴 → 알림 · 0.0s · status 404 (expected 200); ok = null (expected false); breaches = null (expected ["error_rate", "unanswered_rate"]); alert = null (expected sent); message has none of ['error_rate 0.0588 > 0.01']
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 알림 확인 (high · slo) · 0.0s · workflow = null (expected slo); severity = null (expected high); reason = null (expected slo); message has none of ['unanswered_rate']
|
||||||
|
→ request POST http://localhost:8800/alerts/latest
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{}
|
||||||
|
✘ 10분 안에 다시 확인 → 보내지 않음 · 0.0s · status 404 (expected 200); ok = null (expected false); alert = null (expected suppressed)
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ slo 알림은 1개 · 0.0s · count = 0 (expected 1)
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{
|
||||||
|
"workflow": "slo"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
✘ 기준 안이면 알림 없음 · 0.0s · status 404 (expected 200); ok = null (expected true); breaches = null (expected []); alert = null (expected none)
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-slo-alert\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 그래도 알림은 1개 · 0.0s · count = 0 (expected 1)
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{
|
||||||
|
"workflow": "slo"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
Passed 0/6
|
||||||
|
report: runs/lab03-challenge2-20261031-075255.json
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge2.json",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"passed": 6,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "둘째 주 · SLO 못 지킴 → 알림",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"alert": "sent",
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"total": 357
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림 확인 (high · slo)",
|
||||||
|
"webhook": "http://localhost:8800/alerts/latest",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "10분 안에 다시 확인 → 보내지 않음",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"alert": "suppressed",
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"total": 357
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "slo 알림은 1개",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "기준 안이면 알림 없음",
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"alert": "none",
|
||||||
|
"message": "",
|
||||||
|
"total": 309
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "그래도 알림은 1개",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
$ uv run tools/flow.py push answers/77-lab03-challenge2-slo-alert.json
|
||||||
|
✔ pushed '77 SLO alert (answer)' (id w46a402ba838f33c)
|
||||||
|
POST http://localhost:5678/webhook/my-slo-alert
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-slo-alert · 6 case(s) × 1
|
||||||
|
✔ 둘째 주 · SLO 못 지킴 → 알림 · 1.0s
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"alert": "sent",
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"total": 357
|
||||||
|
}
|
||||||
|
✔ 알림 확인 (high · slo) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/alerts/latest
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
✔ 10분 안에 다시 확인 → 보내지 않음 · 0.3s
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-26",
|
||||||
|
"to": "2026-10-30"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"alert": "suppressed",
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"total": 357
|
||||||
|
}
|
||||||
|
✔ slo 알림은 1개 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{
|
||||||
|
"workflow": "slo"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 기준 안이면 알림 없음 · 0.1s
|
||||||
|
→ request POST /webhook/my-slo-alert
|
||||||
|
{
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"alert": "none",
|
||||||
|
"message": "",
|
||||||
|
"total": 309
|
||||||
|
}
|
||||||
|
✔ 그래도 알림은 1개 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{
|
||||||
|
"workflow": "slo"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo",
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"message": "SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476 > 0.02",
|
||||||
|
"at": 1793400787.7696092
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab03-challenge2-20261031-075308.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-slo.json
|
||||||
|
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
|
||||||
|
✔ 둘째 주 · SLO 못 지킴 · 0.1s · 304 calls · $0.039810
|
||||||
|
✔ 첫 주 · 답 못 함만 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 기준을 바꾸면 · 첫 주 · 0.1s · 292 calls · $0.038550
|
||||||
|
✔ 고친 뒤 · 오류 · 답 못 함 기준은 지킴 · 0.1s · 10 calls · $0.000000
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-slo-20261031-075308.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py ops
|
||||||
|
fault switch: all ok
|
||||||
|
live run logs: 30 (tag before 10 · tag after 10 · tag normal 10)
|
||||||
|
added FAQ: 1 (FAQ-007)
|
||||||
|
created tickets: 0
|
||||||
|
DLQ: 0 (parked 0)
|
||||||
|
alerts: 1
|
||||||
|
[high] slo · slo · - · SLO breach: error_rate 0.0588 > 0.01, unanswered_rate 0.0476
|
||||||
|
|
||||||
|
$ uv run tools/flow.py ops --reset
|
||||||
|
✔ live run logs, added FAQ, created tickets, DLQ and alerts emptied, fault switch all ok
|
||||||
|
|
||||||
|
$ 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
|
||||||
|
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||||
|
✔ run logs /logs (666 seeded runs) · FAQ additions /faq-extra (day 77 data API)
|
||||||
|
✔ n8n → http://data-api:8800
|
||||||
|
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||||
|
✔ node catalog (for check)
|
||||||
|
✔ CLI coding agent: codex, claude, agy
|
||||||
|
ready
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "77 Log report + days and releases (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Report webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-log-report",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://data-api:8800/logs/query",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Compute metrics",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send report",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Report webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get logs",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get logs": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Compute metrics",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Compute metrics": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "77 Log report + steps and cost (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Report webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-log-report",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://data-api:8800/logs/query",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Compute metrics",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nout.steps = {};\nfor (const k of ['faq_ms', 'answer_ms']) { const v = rows.map((r) => r.steps?.[k] ?? 0).filter((x) => x > 0); out.steps[k] = { runs: v.length, p50: pct(v, 0.5), p95: pct(v, 0.95) }; }\nout.cost_usd = Math.round(rows.reduce((a, r) => a + (r.cost_usd ?? 0), 0) * 100000) / 100000;\nout.cost_per_run = rows.length ? Math.round((out.cost_usd / rows.length) * 1000000) / 1000000 : 0;\nout.llm_calls = rows.reduce((a, r) => a + (r.llm_calls ?? 0), 0);\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send report",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Report webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get logs",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get logs": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Compute metrics",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Compute metrics": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "77 Log report + repeat topics (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Report webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-log-report",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://data-api:8800/logs/query",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Compute metrics",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const pct = (vals, p) => { const s = [...vals].sort((a, b) => a - b); return s.length ? s[Math.max(0, Math.ceil(p * s.length) - 1)] : 0; };\nconst rate = (a, b) => (b ? Math.round((a / b) * 10000) / 10000 : 0);\nconst rows = $json.logs ?? [];\nconst errors = rows.filter((r) => r.status === 'error').length;\nconst by_route = {};\nfor (const r of rows) { const k = r.route ?? '오류'; by_route[k] = (by_route[k] ?? 0) + 1; }\nconst d = rows.map((r) => r.duration_ms);\nconst out = { total: rows.length, errors, error_rate: rate(errors, rows.length), p50_ms: pct(d, 0.5), p95_ms: pct(d, 0.95), by_route };\nconst group = (key) => { const g = {}; for (const r of rows) (g[key(r)] ??= []).push(r); return g; };\nout.by_day = Object.fromEntries(Object.entries(group((r) => r.started_at.slice(0, 10))).sort().map(([k, v]) => {\n const e = v.filter((r) => r.status === 'error').length; return [k, { total: v.length, errors: e, error_rate: rate(e, v.length) }]; }));\nout.by_release = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const b = (out.by_release[r.release] ??= { answers: 0, repeats: 0 }); b.answers += 1; if (r.repeat) b.repeats += 1; }\nfor (const b of Object.values(out.by_release)) b.repeat_rate = rate(b.repeats, b.answers);\nout.steps = {};\nfor (const k of ['faq_ms', 'answer_ms']) { const v = rows.map((r) => r.steps?.[k] ?? 0).filter((x) => x > 0); out.steps[k] = { runs: v.length, p50: pct(v, 0.5), p95: pct(v, 0.95) }; }\nout.cost_usd = Math.round(rows.reduce((a, r) => a + (r.cost_usd ?? 0), 0) * 100000) / 100000;\nout.cost_per_run = rows.length ? Math.round((out.cost_usd / rows.length) * 1000000) / 1000000 : 0;\nout.llm_calls = rows.reduce((a, r) => a + (r.llm_calls ?? 0), 0);\nconst fails = {};\nfor (const r of rows.filter((x) => x.status === 'error')) { const k = `${r.error_node}|${r.error_message}`; fails[k] = (fails[k] ?? 0) + 1; }\nout.failures = Object.entries(fails).map(([k, count]) => ({ node: k.split('|')[0], message: k.split('|')[1], count })).sort((a, b) => b.count - a.count);\nconst un = {};\nfor (const r of rows.filter((x) => x.route === '담당자 연결')) un[r.topic] = (un[r.topic] ?? 0) + 1;\nout.unanswered_topics = Object.entries(un).map(([topic, count]) => ({ topic, count })).sort((a, b) => b.count - a.count);\nconst hours = {};\nfor (const r of rows) (hours[r.started_at.slice(0, 13)] ??= []).push(r);\nout.anomalies = Object.entries(hours).sort().map(([hour, v]) => { const e = v.filter((r) => r.status === 'error').length; return { hour, total: v.length, errors: e, error_rate: rate(e, v.length) }; })\n .filter((h) => h.total >= 5 && h.error_rate >= 3 * out.error_rate && h.errors > 0);\nconst rep = {};\nfor (const r of rows.filter((x) => x.route === '자동 답변')) { const t = (rep[r.topic] ??= { topic: r.topic, answers: 0, repeats: 0 }); t.answers += 1; if (r.repeat) t.repeats += 1; }\nout.repeat_topics = Object.values(rep).filter((t) => t.repeats > 0).map((t) => ({ ...t, repeat_rate: rate(t.repeats, t.answers) }))\n .sort((a, b) => b.repeat_rate - a.repeat_rate || b.repeats - a.repeats);\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send report",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Report webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get logs",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get logs": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Compute metrics",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Compute metrics": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
{
|
||||||
|
"name": "77 Log summary (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Summary webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-log-summary",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get report",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:5678/webhook/my-log-report",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all' }) }}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Write summary",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||||
|
"typeVersion": 3.1,
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "={{ JSON.stringify({ total: $json.total, errors: $json.errors, error_rate: $json.error_rate, failures: $json.failures, unanswered_topics: $json.unanswered_topics, anomalies: $json.anomalies }) }}",
|
||||||
|
"options": {
|
||||||
|
"systemMessage": "You write a short Korean operations summary for an IT help desk team from the statistics JSON given. Write exactly 3 sentences in 합니다체. Use only numbers that appear in the statistics (you may write a rate like 0.0588 as 5.88%). Sentence 1: the biggest unanswered topic and its count. Sentence 2: the most common failure (node, message, count). Sentence 3: the worst hour from anomalies, written like 10월 26일 09시, with its errors and total. Do not guess causes that are not in the data. Do not add any other numbers.",
|
||||||
|
"maxIterations": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Summary model",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"parameters": {
|
||||||
|
"model": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "id",
|
||||||
|
"value": "gpt-4.1-mini"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
520
|
||||||
|
],
|
||||||
|
"credentials": {
|
||||||
|
"openAiApi": {
|
||||||
|
"id": "openAiCourse0001",
|
||||||
|
"name": "OpenAI (.env)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check numbers",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const stats = $('Get report').first().json;\nconst summary = String($json.output ?? '').trim();\nconst allowed = new Set();\nconst addNum = (v) => { allowed.add(String(v)); for (const k of [100, 10, 1]) allowed.add(String(Math.round(v * 100 * k) / k)); };\nconst walk = (v) => {\n if (Array.isArray(v)) v.forEach(walk);\n else if (v && typeof v === 'object') Object.entries(v).forEach(([k, x]) => { (String(k).match(/\\d+(\\.\\d+)?/g) ?? []).forEach((n) => allowed.add(String(Number(n)))); walk(x); });\n else if (typeof v === 'string') (v.match(/\\d+(\\.\\d+)?/g) ?? []).forEach((n) => allowed.add(String(Number(n))));\n else if (typeof v === 'number') addNum(v);\n};\nwalk(stats);\nconst found = (summary.match(/\\d+(\\.\\d+)?/g) ?? []).map((n) => String(Number(n)));\nconst unknown = [...new Set(found.filter((n) => !allowed.has(n)))];\nconst worst = [...(stats.anomalies ?? [])].sort((a, b) => b.error_rate - a.error_rate)[0];\nreturn [{ json: { summary, numbers_ok: unknown.length === 0, unknown_numbers: unknown,\n top_topic: stats.unanswered_topics?.[0]?.topic ?? null, worst_hour: worst?.hour ?? null } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
760,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send summary",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
980,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Summary webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get report": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Write summary",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Write summary": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check numbers",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check numbers": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send summary",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Summary model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Write summary",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"name": "77 Weekly report (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Weekly webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-weekly",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "This week",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:5678/webhook/my-log-report",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from, to: $json.body.to }) }}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Last week",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:5678/webhook/my-log-report",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $('Weekly webhook').first().json.body.prev_from, to: $('Weekly webhook').first().json.body.prev_to }) }}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Summary",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:5678/webhook/my-log-summary",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $('Weekly webhook').first().json.body.from, to: $('Weekly webhook').first().json.body.to }) }}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 60000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Write report",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const b = $('Weekly webhook').first().json.body;\nconst now = $('This week').first().json, prev = $('Last week').first().json, s = $('Summary').first().json;\nconst pctS = (v) => `${Math.round(v * 10000) / 100}%`;\nconst hour = (h) => `${h.slice(0, 10)} ${h.slice(11, 13)}시`;\nconst slo = (r) => (r.slo?.ok ? '지킴' : '못 지킴 ' + (r.slo?.breaches ?? []).join(', '));\nconst lines = [`# IT 헬프데스크 주간 운영 보고 (${b.from} ~ ${b.to})`, ''];\nlines.push('## 숫자', '', '| 지표 | 지난주 | 이번 주 |', '| --- | --- | --- |');\nlines.push(`| 요청 | ${prev.total} | ${now.total} |`, `| 오류율 | ${pctS(prev.error_rate)} | ${pctS(now.error_rate)} |`,\n `| p95 | ${prev.p95_ms}ms | ${now.p95_ms}ms |`, `| 답 못 함 | ${pctS(prev.unanswered_rate)} | ${pctS(now.unanswered_rate)} |`, `| SLO | ${slo(prev)} | ${slo(now)} |`, '');\nlines.push('## 문제', '');\nfor (const f of (now.failures ?? []).slice(0, 3)) lines.push(`- 실패 ${f.count}건 · ${f.node} · ${f.message}`);\nfor (const u of (now.unanswered_topics ?? []).slice(0, 3)) lines.push(`- 답하지 못한 질문 ${u.count}건 · ${u.topic}`);\nfor (const a of now.anomalies ?? []) lines.push(`- 이상 시간대 ${hour(a.hour)} · ${a.errors}/${a.total} 실패`);\nlines.push('', '## 요약 (LLM, 숫자 확인 ' + (s.numbers_ok ? '통과' : '실패: ' + s.unknown_numbers.join(', ')) + ')', '', s.summary, '');\nlines.push('## 다음 할 일', '');\nif ((now.unanswered_topics ?? [])[0]) lines.push(`- FAQ에 '${now.unanswered_topics[0].topic}' 항목 추가`);\nif ((now.failures ?? []).some((f) => f.node === 'Search FAQ')) lines.push('- FAQ 검색 timeout · 재시도 설정 점검 (72일차)');\nif ((now.failures ?? []).some((f) => /429/.test(f.message))) lines.push('- LLM 사용량 한도 · 대체 경로 점검 (75 · 72일차)');\nreturn [{ json: { markdown: lines.join('\\n'), total: now.total, error_rate: now.error_rate, slo_ok: now.slo?.ok ?? null, numbers_ok: s.numbers_ok } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send weekly",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Weekly webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "This week",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"This week": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Last week",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Last week": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Summary",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Summary": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Write report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Write report": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send weekly",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
{
|
||||||
|
"name": "77 SLO alert (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "SLO webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-slo-alert",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get report",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:5678/webhook/my-log-report",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ from: $json.body.from ?? '', to: $json.body.to ?? '', tag: $json.body.tag ?? '', source: $json.body.source ?? 'all', slo: $json.body.slo ?? {} }) }}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Recent alerts",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://data-api:8800/alerts/list",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'slo', reason: 'slo', within: 600 }) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Decide",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const r = $('Get report').first().json;\nconst recent = $json.count ?? 0;\nconst breaches = r.slo?.breaches ?? [];\nconst message = breaches.length ? 'SLO breach: ' + breaches.map((k) => `${k} ${r[k]} > ${r.slo.limits[k]}`).join(', ') : '';\nconst alert = breaches.length === 0 ? 'none' : recent > 0 ? 'suppressed' : 'sent';\nreturn [{ json: { ok: breaches.length === 0, breaches, alert, message, total: r.total } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send it?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $json.alert === 'sent' }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send alert",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://data-api:8800/alerts",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'slo', severity: 'high', reason: 'slo', breaches: $json.breaches, message: $json.message }) }}",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Result",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{ json: $('Decide').first().json }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1540,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"SLO webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get report": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Recent alerts",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Recent alerts": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Decide",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Decide": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send it?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Send it?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send alert",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Send alert": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Result": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "날짜별 · 월요일",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"by_day.2026-10-26.total": 70,
|
||||||
|
"by_day.2026-10-26.errors": 16,
|
||||||
|
"by_day.2026-10-26.error_rate": 0.2286
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "날짜별 · 목요일",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"by_day.2026-10-29.total": 97,
|
||||||
|
"by_day.2026-10-29.errors": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "날짜는 10일",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"by_day": [
|
||||||
|
"\"2026-10-19\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"by_day": [
|
||||||
|
"\"2026-10-24\"",
|
||||||
|
"\"2026-10-25\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "릴리스별 다시 묻기 · 1.2.0",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"by_release": [
|
||||||
|
"\"1.2.0\": {\"answers\": 8, \"repeats\": 5, \"repeat_rate\": 0.625}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "릴리스별 다시 묻기 · 1.0.0 · 1.1.0",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"by_release": [
|
||||||
|
"\"1.0.0\": {\"answers\": 402, \"repeats\": 19, \"repeat_rate\": 0.0473}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "1.1.0",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"by_release": [
|
||||||
|
"\"1.1.0\": {\"answers\": 181, \"repeats\": 14, \"repeat_rate\": 0.0773}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "두 주 · 단계별 p95",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"steps.faq_ms.p95": 39,
|
||||||
|
"steps.answer_ms.p95": 2509,
|
||||||
|
"steps.answer_ms.p50": 1179
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 단계마다 잰 실행 수",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"steps.faq_ms.runs": 646,
|
||||||
|
"steps.answer_ms.runs": 596
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "월요일 · FAQ 단계가 느림",
|
||||||
|
"request": "requests/monday.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"steps.faq_ms.p95": 1000,
|
||||||
|
"steps.answer_ms.p95": 2482
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 비용",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"cost_usd": 0.07836,
|
||||||
|
"cost_per_run": 0.000118,
|
||||||
|
"llm_calls": 596
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "빈 기간 · 비용 0",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"cost_usd": 0,
|
||||||
|
"cost_per_run": 0,
|
||||||
|
"llm_calls": 0,
|
||||||
|
"steps.faq_ms.p95": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"before": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/logs-reset",
|
||||||
|
"body": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "하루 · from = to (포함)",
|
||||||
|
"request": "requests/monday.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "빈 기간 · 0건이어도 200",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-11-02",
|
||||||
|
"to": "2026-11-06"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0,
|
||||||
|
"p95_ms": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주말 · 실행이 없는 날",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-24",
|
||||||
|
"to": "2026-10-25"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "기간을 비우면 전체",
|
||||||
|
"body": {},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 666
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source seed · 두 주 기록만",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-30",
|
||||||
|
"source": "seed"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 666
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source live · 오늘 실습 기록 (아직 없음)",
|
||||||
|
"body": {
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "두 주 전체",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 666,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"p50_ms": 1205,
|
||||||
|
"p95_ms": 2537
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 갈래별",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"by_route.자동 답변": 591,
|
||||||
|
"by_route.담당자 연결": 34,
|
||||||
|
"by_route.잠시 후": 20,
|
||||||
|
"by_route.오류": 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "첫 주",
|
||||||
|
"request": "requests/week1.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 309,
|
||||||
|
"errors": 0,
|
||||||
|
"error_rate": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "둘째 주",
|
||||||
|
"request": "requests/week2.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 357,
|
||||||
|
"errors": 21,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"p95_ms": 2598
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "10월 26일 하루",
|
||||||
|
"request": "requests/monday.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 70,
|
||||||
|
"errors": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "두 주 · 다시 묻는 비율이 가장 높은 주제",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"repeat_topics.0.topic": "프린터",
|
||||||
|
"repeat_topics.0.answers": 81,
|
||||||
|
"repeat_topics.0.repeats": 8,
|
||||||
|
"repeat_topics.0.repeat_rate": 0.0988
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 수로는 비밀번호가 가장 많지만 비율은 2위",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"repeat_topics.1.topic": "비밀번호",
|
||||||
|
"repeat_topics.1.repeats": 9,
|
||||||
|
"repeat_topics.1.repeat_rate": 0.0647
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 다시 물은 주제는 6개",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"repeat_topics.5.topic": "설치",
|
||||||
|
"repeat_topics.5.repeat_rate": 0.0328
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"repeat_topics": [
|
||||||
|
"와이파이"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "첫 주 · 설치는 다시 묻지 않음",
|
||||||
|
"request": "requests/week1.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"repeat_topics.0.topic": "프린터"
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"repeat_topics": [
|
||||||
|
"설치"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "앞의 지표는 그대로",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 666,
|
||||||
|
"failures.0.count": 16,
|
||||||
|
"anomalies.0.hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-summary",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "요약 · 숫자는 통계에 있는 것만",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"numbers_ok": true,
|
||||||
|
"top_topic": "와이파이",
|
||||||
|
"worst_hour": "2026-10-26T09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "요약 · 주제와 시간대를 말함",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"summary": [
|
||||||
|
"와이파이"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "요약 · 날짜",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"summary": [
|
||||||
|
"10월 26일",
|
||||||
|
"10-26",
|
||||||
|
"26일"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "첫 주 · 오류 0 → 이상 없음",
|
||||||
|
"request": "requests/week1.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0,
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "둘째 주 · 기준 0.0588 × 3",
|
||||||
|
"request": "requests/week2.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"anomalies.0.hour": "2026-10-26T09",
|
||||||
|
"anomalies.1.hour": "2026-10-29T15"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "월요일만 · 기준 0.2286 × 3",
|
||||||
|
"request": "requests/monday.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0.2286,
|
||||||
|
"anomalies.0.hour": "2026-10-26T09",
|
||||||
|
"anomalies.0.errors": 16
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"anomalies": [
|
||||||
|
"2026-10-29T15"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "목요일만 · 15시만",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-29",
|
||||||
|
"to": "2026-10-29"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0.0515,
|
||||||
|
"anomalies.0.hour": "2026-10-29T15",
|
||||||
|
"anomalies.0.total": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "두 주 · 기준 0.0315 × 3",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0.0315,
|
||||||
|
"anomalies.0.hour": "2026-10-26T09",
|
||||||
|
"anomalies.1.hour": "2026-10-29T15"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "예제 1 · 가장 많은 실패",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"failures.0.node": "Search FAQ",
|
||||||
|
"failures.0.message": "timeout of 1000ms exceeded",
|
||||||
|
"failures.0.count": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "예제 1 · 두 번째 실패",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"failures.1.node": "Answer",
|
||||||
|
"failures.1.count": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "예제 1 · 답하지 못한 주제",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"unanswered_topics.0.topic": "와이파이",
|
||||||
|
"unanswered_topics.0.count": 34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "예제 2 · 이상 시간대 1",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"anomalies.0.hour": "2026-10-26T09",
|
||||||
|
"anomalies.0.total": 16,
|
||||||
|
"anomalies.0.errors": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "예제 2 · 이상 시간대 2",
|
||||||
|
"request": "requests/two-weeks.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"anomalies.1.hour": "2026-10-29T15",
|
||||||
|
"anomalies.1.total": 7,
|
||||||
|
"anomalies.1.errors": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "예제 2 · 첫 주에는 없음",
|
||||||
|
"request": "requests/week1.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"anomalies": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-weekly",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 숫자",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 357,
|
||||||
|
"error_rate": 0.0588,
|
||||||
|
"slo_ok": false,
|
||||||
|
"numbers_ok": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 절",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"## 숫자"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 문제 절",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"## 문제"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 다음 할 일",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"## 다음 할 일"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 비교 숫자",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"| 요청 | 309 | 357 |"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 이상 시간대",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"2026-10-26 09시"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "주간 보고 · 와이파이",
|
||||||
|
"request": "requests/weekly.json",
|
||||||
|
"expect": {
|
||||||
|
"contains": {
|
||||||
|
"markdown": [
|
||||||
|
"FAQ에 '와이파이' 항목 추가"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-slo-alert",
|
||||||
|
"before": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/alerts-clear",
|
||||||
|
"body": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "둘째 주 · SLO 못 지킴 → 알림",
|
||||||
|
"request": "requests/week2.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"ok": false,
|
||||||
|
"breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
],
|
||||||
|
"alert": "sent"
|
||||||
|
},
|
||||||
|
"contains": {
|
||||||
|
"message": [
|
||||||
|
"error_rate 0.0588 > 0.01"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "알림 확인 (high · slo)",
|
||||||
|
"body": {},
|
||||||
|
"url": "http://localhost:8800/alerts/latest",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"workflow": "slo",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "slo"
|
||||||
|
},
|
||||||
|
"contains": {
|
||||||
|
"message": [
|
||||||
|
"unanswered_rate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "10분 안에 다시 확인 → 보내지 않음",
|
||||||
|
"request": "requests/week2.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"ok": false,
|
||||||
|
"alert": "suppressed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slo 알림은 1개",
|
||||||
|
"body": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"url": "http://localhost:8800/alerts/list",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "기준 안이면 알림 없음",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"ok": true,
|
||||||
|
"breaches": [],
|
||||||
|
"alert": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "그래도 알림은 1개",
|
||||||
|
"body": {
|
||||||
|
"workflow": "slo"
|
||||||
|
},
|
||||||
|
"url": "http://localhost:8800/alerts/list",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "고치기 전 · 느린 FAQ · 1초에 끊김",
|
||||||
|
"body": {
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"steps.faq_ms.p50": [
|
||||||
|
900,
|
||||||
|
1600
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 느린 FAQ · 기다려서 답함",
|
||||||
|
"body": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"steps.faq_ms.p50": [
|
||||||
|
1150,
|
||||||
|
3500
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 평소 FAQ · 기다릴 일이 없음",
|
||||||
|
"body": {
|
||||||
|
"tag": "normal",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"steps.faq_ms.p50": [
|
||||||
|
0,
|
||||||
|
400
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 답 단계는 FAQ 속도와 상관없음",
|
||||||
|
"body": {
|
||||||
|
"tag": "normal",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"range": {
|
||||||
|
"steps.answer_ms.p50": [
|
||||||
|
300,
|
||||||
|
6000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "둘째 주 · SLO 못 지킴",
|
||||||
|
"request": "requests/week2.json",
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"slo.ok": false,
|
||||||
|
"unanswered_rate": 0.0476
|
||||||
|
},
|
||||||
|
"contains": {
|
||||||
|
"slo": [
|
||||||
|
"\"error_rate\"",
|
||||||
|
"\"unanswered_rate\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"slo": [
|
||||||
|
"\"p95_ms\"]"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "첫 주 · 답 못 함만",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"slo.ok": false,
|
||||||
|
"slo.breaches": [
|
||||||
|
"unanswered_rate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "기준을 바꾸면 · 첫 주",
|
||||||
|
"body": {
|
||||||
|
"from": "2026-10-19",
|
||||||
|
"to": "2026-10-23",
|
||||||
|
"slo": {
|
||||||
|
"unanswered_rate": 0.06
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"slo.ok": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 오류 · 답 못 함 기준은 지킴",
|
||||||
|
"body": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"error_rate": 0,
|
||||||
|
"unanswered_rate": 0
|
||||||
|
},
|
||||||
|
"not_contains": {
|
||||||
|
"slo.breaches": [
|
||||||
|
"error_rate",
|
||||||
|
"unanswered_rate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-log-report",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "고치기 전 · 10건 모두 실패",
|
||||||
|
"body": {
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고치기 전 · FAQ timeout",
|
||||||
|
"body": {
|
||||||
|
"tag": "before",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"failures.0.node": "Search FAQ",
|
||||||
|
"failures.0.count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 오류 없음",
|
||||||
|
"body": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"total": 10,
|
||||||
|
"errors": 0,
|
||||||
|
"by_route.자동 답변": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "고친 뒤 · 답하지 못한 주제 없음",
|
||||||
|
"body": {
|
||||||
|
"tag": "after",
|
||||||
|
"source": "live"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"unanswered_topics": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"before": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/admin/fault",
|
||||||
|
"body": {
|
||||||
|
"faq": "slow",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"after": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/admin/fault",
|
||||||
|
"body": {
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "after · 회의실 와이파이가 안 ",
|
||||||
|
"body": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
},
|
||||||
|
"contains": {
|
||||||
|
"answer": [
|
||||||
|
"CORP-WIFI",
|
||||||
|
"사번"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 사내 Wi-Fi 비밀번",
|
||||||
|
"body": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 와이파이가 자주 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 노트북이 와이파이에 연",
|
||||||
|
"body": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 손님용 wifi도 있나",
|
||||||
|
"body": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · VPN이 자꾸 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 비밀번호를 잊어버렸어요",
|
||||||
|
"body": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 프린터 출력이 안 돼요",
|
||||||
|
"body": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 메일함 용량이 꽉 찼대",
|
||||||
|
"body": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "after · 새 프로그램 설치는 어",
|
||||||
|
"body": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "after"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"before": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/logs-reset",
|
||||||
|
"body": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/faq-reset",
|
||||||
|
"body": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/admin/fault",
|
||||||
|
"body": {
|
||||||
|
"faq": "slow",
|
||||||
|
"slow_seconds": 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"after": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/admin/fault",
|
||||||
|
"body": {
|
||||||
|
"faq": "ok",
|
||||||
|
"tickets": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "before · 회의실 와이파이가 안 ",
|
||||||
|
"body": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 사내 Wi-Fi 비밀번",
|
||||||
|
"body": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 와이파이가 자주 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 노트북이 와이파이에 연",
|
||||||
|
"body": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 손님용 wifi도 있나",
|
||||||
|
"body": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · VPN이 자꾸 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 비밀번호를 잊어버렸어요",
|
||||||
|
"body": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 프린터 출력이 안 돼요",
|
||||||
|
"body": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 메일함 용량이 꽉 찼대",
|
||||||
|
"body": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "before · 새 프로그램 설치는 어",
|
||||||
|
"body": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "before"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"webhook": "my-helpdesk",
|
||||||
|
"before": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8800/admin/fault",
|
||||||
|
"body": {
|
||||||
|
"faq": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"name": "normal · 회의실 와이파이가 안 ",
|
||||||
|
"body": {
|
||||||
|
"question": "회의실 와이파이가 안 잡혀요",
|
||||||
|
"user": "이준호",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 사내 Wi-Fi 비밀번",
|
||||||
|
"body": {
|
||||||
|
"question": "사내 Wi-Fi 비밀번호가 뭐예요?",
|
||||||
|
"user": "박서연",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 와이파이가 자주 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "와이파이가 자주 끊겨요",
|
||||||
|
"user": "최유진",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 노트북이 와이파이에 연",
|
||||||
|
"body": {
|
||||||
|
"question": "노트북이 와이파이에 연결이 안 돼요",
|
||||||
|
"user": "한지훈",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 손님용 wifi도 있나",
|
||||||
|
"body": {
|
||||||
|
"question": "손님용 wifi도 있나요?",
|
||||||
|
"user": "김민지",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · VPN이 자꾸 끊겨요",
|
||||||
|
"body": {
|
||||||
|
"question": "VPN이 자꾸 끊겨요",
|
||||||
|
"user": "정우성",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 비밀번호를 잊어버렸어요",
|
||||||
|
"body": {
|
||||||
|
"question": "비밀번호를 잊어버렸어요",
|
||||||
|
"user": "오세훈",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 프린터 출력이 안 돼요",
|
||||||
|
"body": {
|
||||||
|
"question": "프린터 출력이 안 돼요",
|
||||||
|
"user": "윤하늘",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 메일함 용량이 꽉 찼대",
|
||||||
|
"body": {
|
||||||
|
"question": "메일함 용량이 꽉 찼대요",
|
||||||
|
"user": "강민수",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "normal · 새 프로그램 설치는 어",
|
||||||
|
"body": {
|
||||||
|
"question": "새 프로그램 설치는 어떻게 신청하나요?",
|
||||||
|
"user": "서지아",
|
||||||
|
"tag": "normal"
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"equals": {
|
||||||
|
"route": "자동 답변",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Sample company data API for day 77 (day 76 API + run logs + FAQ additions), attached to the day 65 n8n network
|
||||||
|
# (n8n installed in ~/day65-n8n, Docker project day65-n8n, network day65-n8n_default).
|
||||||
|
# docker compose up -d (run from this folder, after day 65 n8n is running)
|
||||||
|
# from n8n : http://data-api:8800
|
||||||
|
# from WSL : http://localhost:8800
|
||||||
|
name: day67-data
|
||||||
|
|
||||||
|
services:
|
||||||
|
data-api:
|
||||||
|
image: python:3.12-slim
|
||||||
|
command: python /app/server.py
|
||||||
|
environment:
|
||||||
|
- DATA_DIR=/data
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- TZ=KST-9 # live run logs get Korean time (class day), not UTC
|
||||||
|
ports:
|
||||||
|
- "8800:8800"
|
||||||
|
volumes:
|
||||||
|
- ./server.py:/app/server.py:ro
|
||||||
|
- ./data:/data:ro
|
||||||
|
networks:
|
||||||
|
- n8n
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
n8n:
|
||||||
|
external: true
|
||||||
|
name: day65-n8n_default
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 업무용 장비 지급·반납 규정
|
||||||
|
|
||||||
|
## 1. 지급과 교체
|
||||||
|
- 업무용 노트북은 입사일에 지급한다.
|
||||||
|
- 노트북 교체 주기는 4년이다. 고장이나 배터리 교체는 IT 문의로 접수한다.
|
||||||
|
|
||||||
|
## 2. 분실과 도난
|
||||||
|
- 업무용 장비를 잃어버리거나 도난당하면 24시간 이내에 IT 문의와 보안팀에 신고한다.
|
||||||
|
- 신고가 접수되면 IT 담당자가 원격으로 장비의 데이터를 잠근다.
|
||||||
|
|
||||||
|
## 3. 반납
|
||||||
|
- 퇴사하거나 장비를 교체할 때는 5영업일 이내에 장비를 IT 담당자에게 반납한다.
|
||||||
|
- 반납 전에 개인 파일을 지우지 말고, 업무 파일은 사내 드라이브로 옮긴다.
|
||||||
|
|
||||||
|
## 4. 개인 장비 사용
|
||||||
|
- 개인 휴대폰이나 태블릿으로는 업무 메일과 메신저만 쓸 수 있다.
|
||||||
|
- 개인 장비로 업무 메일을 보려면 회사의 모바일 관리 앱(MDM)을 먼저 설치한다.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 메일·문서 보안 규정
|
||||||
|
|
||||||
|
## 1. 메일함
|
||||||
|
- 개인 메일함 용량은 10GB이다. 용량이 90%를 넘으면 안내 메일이 발송된다.
|
||||||
|
- 오래된 메일은 보관함으로 옮겨 용량을 확보한다.
|
||||||
|
|
||||||
|
## 2. 첨부 파일
|
||||||
|
- 메일 첨부 파일은 25MB까지 보낼 수 있다.
|
||||||
|
- 25MB를 넘는 파일은 사내 드라이브에 올리고 공유 링크를 보낸다.
|
||||||
|
- 개인정보가 들어 있는 파일을 외부로 보낼 때는 반드시 암호를 걸고, 암호는 메신저 등 다른 경로로 알려 준다.
|
||||||
|
|
||||||
|
## 3. 문서 등급과 보관
|
||||||
|
- 대외비 문서에는 문서 첫 장에 '대외비'라고 표시한다.
|
||||||
|
- 업무 문서는 3년 동안 보관한 뒤 정리한다.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 회의실 이용 규정
|
||||||
|
|
||||||
|
## 1. 예약
|
||||||
|
- 회의실은 사내 포털에서 예약한다. 한 번에 최대 3시간까지 예약할 수 있다.
|
||||||
|
- 예약 시작 후 10분이 지나도록 아무도 사용하지 않으면 예약이 자동으로 취소된다.
|
||||||
|
|
||||||
|
## 2. 장비
|
||||||
|
- 대회의실의 화상회의 장비를 쓰려면 회의 시작 30분 전까지 IT 문의로 요청한다.
|
||||||
|
- 프로젝터는 B회의실과 대회의실에 있다.
|
||||||
|
|
||||||
|
## 3. 사용 후
|
||||||
|
- 회의가 끝나면 화이트보드를 지우고 의자를 정리한다.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 비밀번호·계정 보안 규정
|
||||||
|
|
||||||
|
## 1. 비밀번호 만들기
|
||||||
|
- 비밀번호는 12자 이상으로 만든다.
|
||||||
|
- 영문 대문자, 영문 소문자, 숫자, 특수문자 중 3종류 이상을 섞는다.
|
||||||
|
- 이름, 생일, 전화번호, 회사 이름처럼 추측하기 쉬운 단어는 쓰지 않는다.
|
||||||
|
|
||||||
|
## 2. 변경 주기
|
||||||
|
- 비밀번호는 90일마다 바꾼다. 만료 7일 전부터 사내 포털에 변경 안내가 표시된다.
|
||||||
|
- 최근에 쓴 비밀번호 5개는 다시 쓸 수 없다.
|
||||||
|
|
||||||
|
## 3. 로그인 실패와 잠금
|
||||||
|
- 로그인에 5번 연속 실패하면 계정이 30분 동안 잠긴다.
|
||||||
|
- 30분이 지나면 자동으로 풀린다. 급한 경우 IT 문의로 잠금 해제를 요청한다.
|
||||||
|
|
||||||
|
## 4. 다단계 인증
|
||||||
|
- 회사 밖에서 사내 시스템에 접속할 때는 OTP 다단계 인증을 반드시 사용한다.
|
||||||
|
- OTP 앱이 설치된 휴대폰을 바꾸면 IT 포털에서 OTP를 다시 등록한다.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 개인정보 처리 지침
|
||||||
|
|
||||||
|
## 1. 수집
|
||||||
|
- 업무에 꼭 필요한 개인정보만 수집한다.
|
||||||
|
- 주민등록번호는 법에서 정한 경우가 아니면 수집하지 않는다.
|
||||||
|
|
||||||
|
## 2. 이용과 보관
|
||||||
|
- 고객 개인정보가 보이는 화면은 캡처하거나 촬영하지 않는다.
|
||||||
|
- 개인정보가 들어 있는 출력물은 다 쓴 뒤 바로 파쇄한다.
|
||||||
|
|
||||||
|
## 3. 유출 대응
|
||||||
|
- 개인정보가 유출되었거나 유출이 의심되면 1시간 이내에 보안팀에 신고한다.
|
||||||
|
- 신고할 때는 유출된 정보의 종류, 대략적인 건수, 발견한 시각을 함께 알린다.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 원격근무·VPN 이용 규정
|
||||||
|
|
||||||
|
## 1. 접속 원칙
|
||||||
|
- 회사 밖에서 사내 시스템에 접속할 때는 반드시 회사 VPN을 먼저 연결한다.
|
||||||
|
- 카페, 공항 같은 공용 와이파이에서는 VPN 없이 사내 시스템에 접속하지 않는다.
|
||||||
|
|
||||||
|
## 2. VPN 인증서
|
||||||
|
- VPN 인증서의 유효 기간은 1년이다.
|
||||||
|
- 만료 30일 전부터 IT 포털의 '인증서 재발급' 메뉴에서 새 인증서를 신청한다.
|
||||||
|
- 인증서가 만료되면 VPN에 접속할 수 없으므로, 만료 전에 재발급한다.
|
||||||
|
|
||||||
|
## 3. 재택근무 중 보안
|
||||||
|
- 자리를 비울 때는 화면을 잠그고, 5분 동안 사용하지 않으면 자동으로 잠기게 설정한다.
|
||||||
|
- 가족과 함께 쓰는 컴퓨터로는 업무를 하지 않는다.
|
||||||
|
- VPN 접속 기록은 1년 동안 보관된다.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 보안 사고 대응 절차
|
||||||
|
|
||||||
|
## 1. 피싱 메일
|
||||||
|
- 보낸 사람이 의심스럽거나 급하게 클릭을 요구하는 메일은 링크를 누르지 않는다.
|
||||||
|
- 메일 화면의 '피싱 신고' 버튼으로 신고한다.
|
||||||
|
- 이미 링크를 눌렀거나 비밀번호를 입력했다면 즉시 비밀번호를 바꾸고 IT 문의로 알린다.
|
||||||
|
|
||||||
|
## 2. 악성코드 감염 의심
|
||||||
|
- 컴퓨터가 갑자기 느려지거나 모르는 프로그램이 실행되면 악성코드 감염을 의심한다.
|
||||||
|
- 감염이 의심되면 먼저 랜선을 뽑고 와이파이를 꺼서 네트워크에서 분리한 뒤 IT 문의로 알린다.
|
||||||
|
- 컴퓨터를 끄거나 파일을 지우지 않는다. 원인 분석에 필요하다.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 소프트웨어 사용 규정
|
||||||
|
|
||||||
|
## 1. 설치와 승인
|
||||||
|
- 업무용 컴퓨터에는 승인된 소프트웨어만 설치한다.
|
||||||
|
- 새 소프트웨어가 필요하면 IT 포털의 '소프트웨어 신청'에서 요청한다. 승인까지 보통 2영업일이 걸린다.
|
||||||
|
- 무료 소프트웨어라도 회사 업무에 쓰려면 상업적 이용이 허용되는지 라이선스를 확인한다.
|
||||||
|
|
||||||
|
## 2. 금지 사항
|
||||||
|
- 불법으로 복제한 소프트웨어는 설치하지 않는다. 적발되면 징계 대상이 된다.
|
||||||
|
|
||||||
|
## 3. 생성형 AI 서비스
|
||||||
|
- 회사가 승인한 생성형 AI 서비스만 업무에 사용한다.
|
||||||
|
- 고객 정보, 개인정보, 대외비 문서는 생성형 AI 서비스에 입력하지 않는다.
|
||||||
|
- 생성형 AI가 만든 결과는 사람이 확인한 뒤에 사용한다.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "이준호",
|
||||||
|
"dept": "영업팀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "박서연",
|
||||||
|
"dept": "재무팀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "최유진",
|
||||||
|
"dept": "인사팀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "한지훈",
|
||||||
|
"dept": "개발팀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "김민지",
|
||||||
|
"dept": "IT팀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "정우성",
|
||||||
|
"dept": "IT팀"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"id": "FAQ-001", "title": "비밀번호 재설정", "keywords": ["비밀번호", "패스워드", "password", "재설정", "초기화"],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."},
|
||||||
|
{"id": "FAQ-002", "title": "계정 잠김 해제", "keywords": ["계정", "잠김", "잠금", "로그인", "실패"],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."},
|
||||||
|
{"id": "FAQ-003", "title": "VPN 접속 오류", "keywords": ["vpn", "원격", "재택", "접속", "연결"],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."},
|
||||||
|
{"id": "FAQ-004", "title": "프린터 출력 안 됨", "keywords": ["프린터", "출력", "인쇄", "용지"],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."},
|
||||||
|
{"id": "FAQ-005", "title": "메일함 용량 초과", "keywords": ["메일", "용량", "메일함", "첨부"],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."},
|
||||||
|
{"id": "FAQ-006", "title": "소프트웨어 설치 요청", "keywords": ["설치", "소프트웨어", "프로그램", "라이선스"],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."}
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"currency": "USD",
|
||||||
|
"unit": "per 1M tokens",
|
||||||
|
"note": "OpenAI 모델 문서의 공개 단가 (2026-10-28 확인). 수업 전에 가격표를 확인해 바꾸세요.",
|
||||||
|
"models": {
|
||||||
|
"gpt-4.1-mini": {"input": 0.40, "output": 1.60},
|
||||||
|
"gpt-5.4-nano": {"input": 0.20, "output": 1.25}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user