79일차 운영 데이터 기반 Ops Assistant: 실습 15개 · 강의(개요 30장 · 단계 문서 · 4단계 정리 · 05 바이브), 정답본 LAB-A · answers · VIBE-A · tops 포함
This commit is contained in:
1 parent
9ce9aceba6
commit
6f0ecd59a9
156 files changed
+31640
No files matched your search
@@ -0,0 +1,31 @@
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ back to the seeded operations morning (alerts · DLQ · quality · usage · budget · release), live logs · added FAQ · tickets emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py off my-ops-facts my-ops-ask my-ops-brief my-ops-eval
|
||||
✔ unpublished my-ops-facts (id w41f9b14835e9a3c)
|
||||
✔ unpublished my-ops-ask (id w48289056bc6d55d)
|
||||
✔ unpublished my-ops-brief (id w871e57398bd7b32)
|
||||
✔ unpublished my-ops-eval (id w7803df55a337611)
|
||||
✔ n8n restarted. Turn it back on with: uv run tools/flow.py push <the workflow file>
|
||||
|
||||
$ uv run tools/flow.py push reference/baseline-ops-facts.json reference/baseline-ops-ask.json
|
||||
✔ pushed '79 Ops facts (team baseline)' (id wbc17136383891fb)
|
||||
POST http://localhost:5678/webhook/base-ops-facts
|
||||
✔ pushed '79 Ops ask (team baseline)' (id w65a1ecde0ce401b)
|
||||
POST http://localhost:5678/webhook/base-ops-ask
|
||||
|
||||
$ 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)
|
||||
✔ schemas faq · employee · ticket, quarantine, quality log (day 78 data API)
|
||||
✔ operations morning /ops/seed-info (day 79 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,37 @@
|
||||
$ uv run tools/flow.py morning
|
||||
seeded morning 2026-11-04T08:30:00 · {"alerts": 6, "dlq": 3, "quality_log": 4, "quarantine": 1, "usage": 192}
|
||||
alerts: 6 (high 3)
|
||||
[high] helpdesk/Search FAQ · timeout · timeout of 1000ms exceeded
|
||||
[high] helpdesk/Answer · rate_limit · 429 Rate limit reached for requests
|
||||
[high] faq-import/Batch ok? · batch_rejected · B-1 rejected
|
||||
[medium] faq-import/Low score? · low_quality · B-2 quality 0.8
|
||||
[medium] faq-import/Low score? · low_quality · B-3 quality 0.8333
|
||||
[medium] intake/Create ticket · api_error · tickets API 503
|
||||
DLQ: 3 (parked 2)
|
||||
#1 attempts 3 parked True tickets API 503
|
||||
#2 attempts 3 parked True tickets API 503
|
||||
#3 attempts 1 parked False timeout
|
||||
quality log: 4 (scores [0, 0.8, 0.8333, 1]) · quarantine 1
|
||||
usage: 156 requests · 192 calls · $0.073604
|
||||
budget: used $0.073604 of $0.08
|
||||
release: active 1.1.0 · canary {"release": "1.2.0", "percent": 20}
|
||||
|
||||
$ uv run tools/flow.py call base-ops-facts requests/week2.json
|
||||
HTTP 200 · 1.4s
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"cases": "lab01.json",
|
||||
"webhook": "base-ops-facts",
|
||||
"passed": 0,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "지난주 실행 (기본 기간)",
|
||||
"webhook": "base-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"period.from = null (expected 2026-10-26)",
|
||||
"runs.total = 666 (expected 357)",
|
||||
"runs.error_rate = 3.153153153153153 (expected 0.0588)",
|
||||
"runs.p95_ms = null (expected 2598)",
|
||||
"runs.top_failure.node = null (expected Search FAQ)",
|
||||
"runs.top_failure.count = null (expected 16)"
|
||||
],
|
||||
"seconds": 0.4,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "지금 상태 · 알림 · DLQ · 품질",
|
||||
"webhook": "base-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"alerts.high = null (expected 3)",
|
||||
"alerts.by_reason.low_quality = null (expected 2)",
|
||||
"dlq.parked = null (expected 2)",
|
||||
"quality.avg_score = null (expected 0.6583)",
|
||||
"quality.last_score = null (expected 1)",
|
||||
"quality.quarantine = null (expected 1)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "비용 · 릴리스",
|
||||
"webhook": "base-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"cost.requests = null (expected 156)",
|
||||
"cost.used_ratio = null (expected 0.9201)",
|
||||
"cost.top_user.user = null (expected test-bot)",
|
||||
"cost.top_user.share = null (expected 0.3699)",
|
||||
"release.active = null (expected 1.1.0)",
|
||||
"release.canary_release = null (expected 1.2.0)",
|
||||
"release.canary_repeat_rate = null (expected 0.625)",
|
||||
"release.active_repeat_rate = null (expected 0.0773)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "첫 주 (기간을 바꾸면 실행만 바뀜)",
|
||||
"webhook": "base-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"runs.total = 666 (expected 309)",
|
||||
"runs.errors = 21 (expected 0)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"response": {
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "상태가 바뀌면 숫자도 (DLQ #3 처리)",
|
||||
"webhook": "base-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"dlq.waiting = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 2
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
$ uv run tools/flow.py test cases/lab01.json --webhook base-ops-facts -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/base-ops-facts · 5 case(s) × 1
|
||||
✘ 지난주 실행 (기본 기간) · 0.4s · period.from = null (expected 2026-10-26); runs.total = 666 (expected 357); runs.error_rate = 3.153153153153153 (expected 0.0588); runs.p95_ms = null (expected 2598); runs.top_failure.node = null (expected Search FAQ); runs.top_failure.count = null (expected 16)
|
||||
→ request POST /webhook/base-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
✘ 지금 상태 · 알림 · DLQ · 품질 · 0.1s · alerts.high = null (expected 3); alerts.by_reason.low_quality = null (expected 2); dlq.parked = null (expected 2); quality.avg_score = null (expected 0.6583); quality.last_score = null (expected 1); quality.quarantine = null (expected 1)
|
||||
→ request POST /webhook/base-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
✘ 비용 · 릴리스 · 0.1s · cost.requests = null (expected 156); cost.used_ratio = null (expected 0.9201); cost.top_user.user = null (expected test-bot); cost.top_user.share = null (expected 0.3699); release.active = null (expected 1.1.0); release.canary_release = null (expected 1.2.0); release.canary_repeat_rate = null (expected 0.625); release.active_repeat_rate = null (expected 0.0773)
|
||||
→ request POST /webhook/base-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
✘ 첫 주 (기간을 바꾸면 실행만 바뀜) · 0.1s · runs.total = 666 (expected 309); runs.errors = 21 (expected 0)
|
||||
→ request POST /webhook/base-ops-facts
|
||||
{
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 3,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 2
|
||||
}
|
||||
✘ 상태가 바뀌면 숫자도 (DLQ #3 처리) · 0.1s · dlq.waiting = null (expected 0)
|
||||
→ request POST /webhook/base-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 3.153153153153153
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6
|
||||
},
|
||||
"dlq": {
|
||||
"size": 2
|
||||
},
|
||||
"cost": {
|
||||
"used_usd": 0.073604,
|
||||
"daily_usd": 0.08
|
||||
}
|
||||
}
|
||||
Passed 0/5
|
||||
report: runs/lab01-20261101-185111.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
{
|
||||
"cases": "lab01.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "지난주 실행 (기본 기간)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "지금 상태 · 알림 · DLQ · 품질",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.4,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "비용 · 릴리스",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "첫 주 (기간을 바꾸면 실행만 바뀜)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "상태가 바뀌면 숫자도 (DLQ #3 처리)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 2,
|
||||
"parked": 2,
|
||||
"waiting": 0
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
$ uv run tools/flow.py push reference/lab01-example2-facts.json
|
||||
✔ pushed '79 Ops facts (reference)' (id w91aa244110abf96)
|
||||
POST http://localhost:5678/webhook/my-ops-facts
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 지난주 실행 (기본 기간) · 1.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ 지금 상태 · 알림 · DLQ · 품질 · 0.4s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ 비용 · 릴리스 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ 첫 주 (기간을 바꾸면 실행만 바뀜) · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 3,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 2
|
||||
}
|
||||
✔ 상태가 바뀌면 숫자도 (DLQ #3 처리) · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 2,
|
||||
"parked": 2,
|
||||
"waiting": 0
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab01-20261101-185125.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py call my-ops-facts requests/week2.json
|
||||
HTTP 200 · 0.1s
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"cases": "lab01-period.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "두 주 (10/19~10/30)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0315,
|
||||
"p95_ms": 2537,
|
||||
"unanswered_rate": 0.0511,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "하루 (10/26)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-26"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-26"
|
||||
},
|
||||
"runs": {
|
||||
"total": 70,
|
||||
"errors": 16,
|
||||
"error_rate": 0.2286,
|
||||
"p95_ms": 2468,
|
||||
"unanswered_rate": 0.0571,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "빈 기간 (11/02~11/06) · 0건이어도 200",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"runs": {
|
||||
"total": 0,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 0,
|
||||
"unanswered_rate": 0,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "기간을 비우면 지난주 (기본)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "기간과 상관없는 '지금' 숫자는 그대로",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"runs": {
|
||||
"total": 0,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 0,
|
||||
"unanswered_rate": 0,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
$ uv run tools/flow.py test cases/lab01-period.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 두 주 (10/19~10/30) · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-30"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 666,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0315,
|
||||
"p95_ms": 2537,
|
||||
"unanswered_rate": 0.0511,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ 하루 (10/26) · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-26"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-26"
|
||||
},
|
||||
"runs": {
|
||||
"total": 70,
|
||||
"errors": 16,
|
||||
"error_rate": 0.2286,
|
||||
"p95_ms": 2468,
|
||||
"unanswered_rate": 0.0571,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
✔ 빈 기간 (11/02~11/06) · 0건이어도 200 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"runs": {
|
||||
"total": 0,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 0,
|
||||
"unanswered_rate": 0,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
✔ 기간을 비우면 지난주 (기본) · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ 기간과 상관없는 '지금' 숫자는 그대로 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-11-02",
|
||||
"to": "2026-11-06"
|
||||
},
|
||||
"runs": {
|
||||
"total": 0,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 0,
|
||||
"unanswered_rate": 0,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab01-period-20261101-185126.json
|
||||
@@ -0,0 +1,377 @@
|
||||
{
|
||||
"cases": "lab01-challenge.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 0,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "지난주 영역별 상태",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"health.runs = null (expected red)",
|
||||
"health.alerts = null (expected yellow)",
|
||||
"health.dlq = null (expected red)",
|
||||
"health.quality = null (expected yellow)",
|
||||
"health.cost = null (expected yellow)",
|
||||
"health.release = null (expected red)",
|
||||
"overall = null (expected red)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "빨강 · 노랑 목록 (정해진 순서)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"red = null (expected [\"runs\", \"dlq\", \"release\"])",
|
||||
"yellow = null (expected [\"alerts\", \"quality\", \"cost\"])"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"health.runs = null (expected yellow)",
|
||||
"health.release = null (expected yellow)",
|
||||
"red = null (expected [\"dlq\"])"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예산을 늘리면 비용은 초록",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"health.cost = null (expected green)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "parked를 처리하면 DLQ는 노랑",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"health.dlq = null (expected yellow)",
|
||||
"red = null (expected [\"runs\", \"release\"])"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 1,
|
||||
"parked": 0,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✘ 지난주 영역별 상태 · 0.1s · health.runs = null (expected red); health.alerts = null (expected yellow); health.dlq = null (expected red); health.quality = null (expected yellow); health.cost = null (expected yellow); health.release = null (expected red); overall = null (expected red)
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✘ 빨강 · 노랑 목록 (정해진 순서) · 0.1s · red = null (expected ["runs", "dlq", "release"]); yellow = null (expected ["alerts", "quality", "cost"])
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✘ 첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가 · 0.1s · health.runs = null (expected yellow); health.release = null (expected yellow); red = null (expected ["dlq"])
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.073604, "remaining_usd": 0.926396,
|
||||
→ request POST http://localhost:8800/budget
|
||||
{
|
||||
"daily_usd": 1
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"daily_usd": 1.0,
|
||||
"user_limit_per_min": 20,
|
||||
"max_question_chars": 500,
|
||||
"used_usd": 0.073604,
|
||||
"remaining_usd": 0.926396,
|
||||
"over": false
|
||||
}
|
||||
✘ 예산을 늘리면 비용은 초록 · 0.1s · health.cost = null (expected green)
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 1,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 1}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 2,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 1
|
||||
}
|
||||
✘ parked를 처리하면 DLQ는 노랑 · 0.1s · health.dlq = null (expected yellow); red = null (expected ["runs", "release"])
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 1,
|
||||
"parked": 0,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
}
|
||||
}
|
||||
Passed 0/5
|
||||
report: runs/lab01-challenge-20261101-185126.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
{
|
||||
"cases": "lab01-challenge.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "지난주 영역별 상태",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "빨강 · 노랑 목록 (정해진 순서)",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.3,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
},
|
||||
"health": {
|
||||
"runs": "yellow",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "yellow"
|
||||
},
|
||||
"red": [
|
||||
"dlq"
|
||||
],
|
||||
"yellow": [
|
||||
"runs",
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost",
|
||||
"release"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예산을 늘리면 비용은 초록",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "green",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "parked를 처리하면 DLQ는 노랑",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 1,
|
||||
"parked": 0,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "yellow",
|
||||
"quality": "yellow",
|
||||
"cost": "green",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
$ uv run tools/flow.py push answers/79-lab01-challenge-health.json
|
||||
! unpublished '79 Ops facts (reference)' (w91aa244110abf96): it used the same webhook path
|
||||
✔ pushed '79 Ops facts + health (answer)' (id wed38f213f1753a6)
|
||||
POST http://localhost:5678/webhook/my-ops-facts
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.1s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 지난주 영역별 상태 · 1.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ 빨강 · 노랑 목록 (정해진 순서) · 0.3s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ 첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-19",
|
||||
"to": "2026-10-23"
|
||||
},
|
||||
"runs": {
|
||||
"total": 309,
|
||||
"errors": 0,
|
||||
"error_rate": 0,
|
||||
"p95_ms": 2525,
|
||||
"unanswered_rate": 0.055,
|
||||
"top_failure": null
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": null,
|
||||
"canary_repeat_rate": null
|
||||
},
|
||||
"health": {
|
||||
"runs": "yellow",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "yellow"
|
||||
},
|
||||
"red": [
|
||||
"dlq"
|
||||
],
|
||||
"yellow": [
|
||||
"runs",
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost",
|
||||
"release"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.073604, "remaining_usd": 0.926396,
|
||||
→ request POST http://localhost:8800/budget
|
||||
{
|
||||
"daily_usd": 1
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"daily_usd": 1.0,
|
||||
"user_limit_per_min": 20,
|
||||
"max_question_chars": 500,
|
||||
"used_usd": 0.073604,
|
||||
"remaining_usd": 0.926396,
|
||||
"over": false
|
||||
}
|
||||
✔ 예산을 늘리면 비용은 초록 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "green",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 1,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 1}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 2,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 1
|
||||
}
|
||||
✔ parked를 처리하면 DLQ는 노랑 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 1,
|
||||
"parked": 0,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 1,
|
||||
"used_ratio": 0.0736,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "yellow",
|
||||
"quality": "yellow",
|
||||
"cost": "green",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab01-challenge-20261101-185142.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 지난주 실행 (기본 기간) · 0.1s
|
||||
✔ 지금 상태 · 알림 · DLQ · 품질 · 0.1s
|
||||
✔ 비용 · 릴리스 · 0.1s
|
||||
✔ 첫 주 (기간을 바꾸면 실행만 바뀜) · 0.1s
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
✔ 상태가 바뀌면 숫자도 (DLQ #3 처리) · 0.1s
|
||||
Passed 5/5
|
||||
report: runs/lab01-20261101-185143.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"cases": "lab01-challenge2.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 0,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "비용 출처가 멈춰도 200 · 모르는 것은 모른다고",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 200)",
|
||||
"sources_down = null (expected [\"cost\"])",
|
||||
"health.cost = null (expected unknown)",
|
||||
"unknown = null (expected [\"cost\"])"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "다른 영역은 그대로",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 200)",
|
||||
"dlq.size = null (expected 3)",
|
||||
"health.dlq = null (expected red)",
|
||||
"red = null (expected [\"runs\", \"dlq\", \"release\"])",
|
||||
"overall = null (expected red)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "출처가 돌아오면 다시 노랑",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"sources_down = null (expected [])",
|
||||
"unknown = null (expected [])"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "error", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "error",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 3 case(s) × 1
|
||||
✘ 비용 출처가 멈춰도 200 · 모르는 것은 모른다고 · 0.1s · status 500 (expected 200); sources_down = null (expected ["cost"]); health.cost = null (expected unknown); unknown = null (expected ["cost"])
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
✘ 다른 영역은 그대로 · 0.1s · status 500 (expected 200); dlq.size = null (expected 3); health.dlq = null (expected red); red = null (expected ["runs", "dlq", "release"]); overall = null (expected red)
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 출처가 돌아오면 다시 노랑 · 0.1s · sources_down = null (expected []); unknown = null (expected [])
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
Passed 0/3
|
||||
report: runs/lab01-challenge2-20261101-185143.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"cases": "lab01-challenge2.json",
|
||||
"webhook": "my-ops-facts",
|
||||
"passed": 3,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "비용 출처가 멈춰도 200 · 모르는 것은 모른다고",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": null,
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [
|
||||
"cost"
|
||||
],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "unknown",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"unknown": [
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "다른 영역은 그대로",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.3,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": null,
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [
|
||||
"cost"
|
||||
],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "unknown",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"unknown": [
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "출처가 돌아오면 다시 노랑",
|
||||
"webhook": "my-ops-facts",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"unknown": [],
|
||||
"overall": "red"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
$ uv run tools/flow.py push answers/79-lab01-challenge2-sources.json
|
||||
! unpublished '79 Ops facts + health (answer)' (wed38f213f1753a6): it used the same webhook path
|
||||
✔ pushed '79 Ops facts + sources down (answer)' (id w5657987a9503a11)
|
||||
POST http://localhost:5678/webhook/my-ops-facts
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "error", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "error",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 3 case(s) × 1
|
||||
✔ 비용 출처가 멈춰도 200 · 모르는 것은 모른다고 · 1.2s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": null,
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [
|
||||
"cost"
|
||||
],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "unknown",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"unknown": [
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ 다른 영역은 그대로 · 0.3s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": null,
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [
|
||||
"cost"
|
||||
],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "unknown",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality"
|
||||
],
|
||||
"unknown": [
|
||||
"cost"
|
||||
],
|
||||
"overall": "red"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 출처가 돌아오면 다시 노랑 · 0.1s
|
||||
→ request POST /webhook/my-ops-facts
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"period": {
|
||||
"from": "2026-10-26",
|
||||
"to": "2026-10-30"
|
||||
},
|
||||
"runs": {
|
||||
"total": 357,
|
||||
"errors": 21,
|
||||
"error_rate": 0.0588,
|
||||
"p95_ms": 2598,
|
||||
"unanswered_rate": 0.0476,
|
||||
"top_failure": {
|
||||
"node": "Search FAQ",
|
||||
"message": "timeout of 1000ms exceeded",
|
||||
"count": 16
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"count": 6,
|
||||
"high": 3,
|
||||
"by_reason": {
|
||||
"timeout": 1,
|
||||
"rate_limit": 1,
|
||||
"batch_rejected": 1,
|
||||
"low_quality": 2,
|
||||
"api_error": 1
|
||||
}
|
||||
},
|
||||
"dlq": {
|
||||
"size": 3,
|
||||
"parked": 2,
|
||||
"waiting": 1
|
||||
},
|
||||
"quality": {
|
||||
"batches": 4,
|
||||
"avg_score": 0.6583,
|
||||
"last_score": 1,
|
||||
"rejected_batches": 1,
|
||||
"quarantine": 1
|
||||
},
|
||||
"cost": {
|
||||
"requests": 156,
|
||||
"calls": 192,
|
||||
"cost_usd": 0.073604,
|
||||
"daily_usd": 0.08,
|
||||
"used_ratio": 0.9201,
|
||||
"top_user": {
|
||||
"user": "test-bot",
|
||||
"cost_usd": 0.027228,
|
||||
"share": 0.3699
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"active": "1.1.0",
|
||||
"canary_release": "1.2.0",
|
||||
"canary_percent": 20,
|
||||
"active_repeat_rate": 0.0773,
|
||||
"canary_repeat_rate": 0.625
|
||||
},
|
||||
"sources_down": [],
|
||||
"health": {
|
||||
"runs": "red",
|
||||
"alerts": "yellow",
|
||||
"dlq": "red",
|
||||
"quality": "yellow",
|
||||
"cost": "yellow",
|
||||
"release": "red"
|
||||
},
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"unknown": [],
|
||||
"overall": "red"
|
||||
}
|
||||
Passed 3/3
|
||||
report: runs/lab01-challenge2-20261101-185159.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"usage": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 지난주 실행 (기본 기간) · 0.1s
|
||||
✔ 지금 상태 · 알림 · DLQ · 품질 · 0.1s
|
||||
✔ 비용 · 릴리스 · 0.1s
|
||||
✔ 첫 주 (기간을 바꾸면 실행만 바뀜) · 0.1s
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
✔ 상태가 바뀌면 숫자도 (DLQ #3 처리) · 0.1s
|
||||
Passed 5/5
|
||||
report: runs/lab01-20261101-185159.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 지난주 영역별 상태 · 0.1s
|
||||
✔ 빨강 · 노랑 목록 (정해진 순서) · 0.1s
|
||||
✔ 첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가 · 0.1s
|
||||
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.073604, "remaining_usd": 0.926396,
|
||||
✔ 예산을 늘리면 비용은 초록 · 0.1s
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 1}
|
||||
✔ parked를 처리하면 DLQ는 노랑 · 0.1s
|
||||
Passed 5/5
|
||||
report: runs/lab01-challenge-20261101-185200.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-period.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
|
||||
✔ 두 주 (10/19~10/30) · 0.1s
|
||||
✔ 하루 (10/26) · 0.1s
|
||||
✔ 빈 기간 (11/02~11/06) · 0건이어도 200 · 0.1s
|
||||
✔ 기간을 비우면 지난주 (기본) · 0.1s
|
||||
✔ 기간과 상관없는 '지금' 숫자는 그대로 · 0.1s
|
||||
Passed 5/5
|
||||
report: runs/lab01-period-20261101-185201.json
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "base-ops-ask",
|
||||
"passed": 0,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "실행 · 오류율",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = null (expected runs)",
|
||||
"refused = null (expected false)"
|
||||
],
|
||||
"seconds": 2.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"answer": "지난주 오류율은 5.88%입니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ · parked",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = null (expected dlq)",
|
||||
"refused = null (expected false)"
|
||||
],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "운영과 상관없는 질문은 거절",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = null (expected unknown)",
|
||||
"refused = null (expected true)",
|
||||
"reason = null (expected out_of_scope)"
|
||||
],
|
||||
"seconds": 0.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"answer": "오늘 점심 메뉴로는 김치찌개, 불고기, 비빔밥 중에서 선택해 보시는 건 어떨까요? 따뜻하고 든든한 한 끼가 될 거예요!"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "작업 요청은 거절 (읽기만)",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"refused = null (expected true)",
|
||||
"reason = null (expected read_only)"
|
||||
],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"answer": "DLQ 1번 작업을 다시 실행해 드리겠습니다. 잠시만 기다려 주세요. \n작업이 완료되면 다시 안내해 드리겠습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "비용 · 숫자 확인",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = null (expected cost)",
|
||||
"numbers_ok = null (expected true)"
|
||||
],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"answer": "오늘 LLM 비용은 예산의 약 92.01%를 사용했습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "릴리스 · 숫자 확인",
|
||||
"webhook": "base-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = null (expected release)",
|
||||
"numbers_ok = null (expected true)",
|
||||
"answer has none of ['62.5']"
|
||||
],
|
||||
"seconds": 2.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"answer": "카나리 버전(1.2.0)에서 문제가 일부 발견되고 있습니다. 전체 실행 중 오류율은 약 5.88%로 다소 높은 편이며, 특히 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과 오류가 16건 발생했습니다. 알림도 6건으로 다수 존재하며, 타임아웃, 속도 제한, 배치 거부 등 다양한 원인이 포함되어 있습니다. DLQ(Dead Letter Queue) 상태도 좋지 않고, 품질 지표도 낮은 편입니다. 현재 전체 시스템 상태가 빨간색으로 표시되어 있어, 카나리 버전에서 안정성 문제가 있다고 판단됩니다. 따라서 카나리 버전 적용에 신중을 기할 필요가 있습니다."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
$ uv run tools/flow.py test cases/lab02.json --webhook base-ops-ask -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/base-ops-ask · 6 case(s) × 1
|
||||
✘ 실행 · 오류율 · 2.7s · area = null (expected runs); refused = null (expected false)
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"answer": "지난주 오류율은 5.88%입니다."
|
||||
}
|
||||
✘ DLQ · parked · 1.0s · area = null (expected dlq); refused = null (expected false)
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다."
|
||||
}
|
||||
✘ 운영과 상관없는 질문은 거절 · 0.9s · area = null (expected unknown); refused = null (expected true); reason = null (expected out_of_scope)
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"answer": "오늘 점심 메뉴로는 김치찌개, 불고기, 비빔밥 중에서 선택해 보시는 건 어떨까요? 따뜻하고 든든한 한 끼가 될 거예요!"
|
||||
}
|
||||
✘ 작업 요청은 거절 (읽기만) · 1.2s · refused = null (expected true); reason = null (expected read_only)
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"answer": "DLQ 1번 작업을 다시 실행해 드리겠습니다. 잠시만 기다려 주세요. \n작업이 완료되면 다시 안내해 드리겠습니다."
|
||||
}
|
||||
✘ 비용 · 숫자 확인 · 1.2s · area = null (expected cost); numbers_ok = null (expected true)
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"answer": "오늘 LLM 비용은 예산의 약 92.01%를 사용했습니다."
|
||||
}
|
||||
✘ 릴리스 · 숫자 확인 · 2.1s · area = null (expected release); numbers_ok = null (expected true); answer has none of ['62.5']
|
||||
→ request POST /webhook/base-ops-ask
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"answer": "카나리 버전(1.2.0)에서 문제가 일부 발견되고 있습니다. 전체 실행 중 오류율은 약 5.88%로 다소 높은 편이며, 특히 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과 오류가 16건 발생했습니다. 알림도 6건으로 다수 존재하며, 타임아웃, 속도 제한, 배치 거부 등 다양한 원인이 포함되어 있습니다. DLQ(Dead Letter Queue) 상태도 좋지 않고, 품질 지표도 낮은 편입니다. 현재 전체 시스템 상태가 빨간색으로 표시되어 있어, 카나리 버전에서 안정성 문제가 있다고 판단됩니다. 따라서 카나리 버전 적용에 신중을 기할 필요가 있습니다."
|
||||
}
|
||||
Passed 0/6
|
||||
report: runs/lab02-20261101-185220.json
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 3,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "실행 · 오류율",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ · parked",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다. 전체 DLQ 크기는 3건이며, 대기 중인 건은 1건입니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "운영과 상관없는 질문은 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "작업 요청은 거절 (읽기만)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"refused = false (expected true)",
|
||||
"reason = null (expected read_only)"
|
||||
],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ 1번은 현재 대기 중인 상태이며, DLQ 전체 크기는 3입니다. 현재 시스템 상태는 빨간색(위험)입니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "비용 · 숫자 확인",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"numbers_ok = null (expected true)"
|
||||
],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "오늘 LLM 비용은 예산의 92.01%에 해당합니다. 일일 비용은 0.08달러이며, 현재 사용 비율은 0.9201입니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "릴리스 · 숫자 확인",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"numbers_ok = null (expected true)"
|
||||
],
|
||||
"seconds": 2.3,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "카나리 버전 1.2.0의 반복 발생률은 62.5%로 매우 높으며, 현재 전체 시스템 상태는 빨간색(문제 있음)입니다. 활성 버전 1.1.0의 반복 발생률은 7.73%에 비해 카나리 버전에서 문제가 심각한 것으로 나타납니다."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
$ uv run tools/flow.py push reference/lab02-example1-ask.json
|
||||
✔ pushed '79 Ops ask (reference)' (id w7ab48263bf09133)
|
||||
POST http://localhost:5678/webhook/my-ops-ask
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ 실행 · 오류율 · 2.9s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다."
|
||||
}
|
||||
✔ DLQ · parked · 1.8s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다. 전체 DLQ 크기는 3건이며, 대기 중인 건은 1건입니다."
|
||||
}
|
||||
✔ 운영과 상관없는 질문은 거절 · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✘ 작업 요청은 거절 (읽기만) · 1.0s · refused = false (expected true); reason = null (expected read_only)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ 1번은 현재 대기 중인 상태이며, DLQ 전체 크기는 3입니다. 현재 시스템 상태는 빨간색(위험)입니다."
|
||||
}
|
||||
✘ 비용 · 숫자 확인 · 1.0s · numbers_ok = null (expected true)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "오늘 LLM 비용은 예산의 92.01%에 해당합니다. 일일 비용은 0.08달러이며, 현재 사용 비율은 0.9201입니다."
|
||||
}
|
||||
✘ 릴리스 · 숫자 확인 · 2.3s · numbers_ok = null (expected true)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "카나리 버전 1.2.0의 반복 발생률은 62.5%로 매우 높으며, 현재 전체 시스템 상태는 빨간색(문제 있음)입니다. 활성 버전 1.1.0의 반복 발생률은 7.73%에 비해 카나리 버전에서 문제가 심각한 것으로 나타납니다."
|
||||
}
|
||||
Passed 3/6
|
||||
report: runs/lab02-20261101-185243.json
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "실행 · 오류율",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ · parked",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다. 전체 DLQ 크기는 3건이며, 대기 중인 건은 1건입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "운영과 상관없는 질문은 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "작업 요청은 거절 (읽기만)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "비용 · 숫자 확인",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "오늘 LLM 비용은 예산의 92.01%에 해당합니다. 일일 비용은 0.08달러이며, 현재 사용 비율은 0.9201입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "릴리스 · 숫자 확인",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.5,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "카나리 버전 1.2.0의 반복 발생률은 62.5%로 매우 높아 문제가 있는 것으로 보입니다. 현재 전체 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
$ uv run tools/flow.py push reference/lab02-example2-ask.json
|
||||
! unpublished '79 Ops ask (reference)' (w7ab48263bf09133): it used the same webhook path
|
||||
✔ pushed '79 Ops ask + refuse + number check (reference)' (id w48289056bc6d55d)
|
||||
POST http://localhost:5678/webhook/my-ops-ask
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ 실행 · 오류율 · 2.7s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ DLQ · parked · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "DLQ에 parked 된 건은 2건입니다. 전체 DLQ 크기는 3건이며, 대기 중인 건은 1건입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 운영과 상관없는 질문은 거절 · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 작업 요청은 거절 (읽기만) · 0.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
✔ 비용 · 숫자 확인 · 1.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "오늘 LLM 비용은 예산의 92.01%에 해당합니다. 일일 비용은 0.08달러이며, 현재 사용 비율은 0.9201입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 릴리스 · 숫자 확인 · 1.5s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "카나리 버전 1.2.0의 반복 발생률은 62.5%로 매우 높아 문제가 있는 것으로 보입니다. 현재 전체 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab02-20261101-185304.json
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"cases": "lab02-phrasing.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "'에러' → 실행",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "에러 많이 났어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "에러 많이 났어?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 총 357회 실행 중 21회 에러가 발생하여 에러율은 5.88%입니다. 주요 실패 원인은 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과로 16회 발생했습니다. 현재 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "'장애' 는 낱말 목록에 없음 → 운영 밖으로 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "장애 났어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "장애 났어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "두 영역이 한 번씩 → 앞의 영역 (알림)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘",
|
||||
"area": "alerts",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "알림은 총 6건이며, 그중 3건은 높은 심각도입니다. DLQ 관련 정보는 제공된 사실에 포함되어 있지 않습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "'꺼 줘' → 작업 요청으로 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "카나리 꺼 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "카나리 꺼 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "'배포'(1) 보다 '상태 · 요약'(2) → 전체",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "배포 상태 요약해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "배포 상태 요약해 줘",
|
||||
"area": "overview",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 총 357회 실행 중 오류율은 5.88%이며, 주요 실패 원인은 'Search FAQ' 노드에서 1000ms 타임아웃 초과로 16건 발생했습니다. 경고는 총 6건으로 고위험 3건, 낮은 품질과 API 오류 등이 포함되어 있고, DLQ에는 3건이 대기 중입니다. 전체 배포 상태는 실행, DLQ, 릴리스가 빨간색으로 심각하며, 경고, 품질, 비용은 노란색으로 나타나 전체적으로는 빨간색 상태입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말 없는 비용 질문 → 거절 (과제 1에서 LLM 라우터로)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
$ uv run tools/flow.py test cases/lab02-phrasing.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ '에러' → 실행 · 1.2s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "에러 많이 났어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "에러 많이 났어?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 총 357회 실행 중 21회 에러가 발생하여 에러율은 5.88%입니다. 주요 실패 원인은 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과로 16회 발생했습니다. 현재 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ '장애' 는 낱말 목록에 없음 → 운영 밖으로 거절 · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "장애 났어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "장애 났어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 두 영역이 한 번씩 → 앞의 영역 (알림) · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘",
|
||||
"area": "alerts",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "알림은 총 6건이며, 그중 3건은 높은 심각도입니다. DLQ 관련 정보는 제공된 사실에 포함되어 있지 않습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ '꺼 줘' → 작업 요청으로 거절 · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "카나리 꺼 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "카나리 꺼 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
✔ '배포'(1) 보다 '상태 · 요약'(2) → 전체 · 1.7s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "배포 상태 요약해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "배포 상태 요약해 줘",
|
||||
"area": "overview",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 총 357회 실행 중 오류율은 5.88%이며, 주요 실패 원인은 'Search FAQ' 노드에서 1000ms 타임아웃 초과로 16건 발생했습니다. 경고는 총 6건으로 고위험 3건, 낮은 품질과 API 오류 등이 포함되어 있고, DLQ에는 3건이 대기 중입니다. 전체 배포 상태는 실행, DLQ, 릴리스가 빨간색으로 심각하며, 경고, 품질, 비용은 노란색으로 나타나 전체적으로는 빨간색 상태입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 낱말 없는 비용 질문 → 거절 (과제 1에서 LLM 라우터로) · 0.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab02-phrasing-20261101-185309.json
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"cases": "lab02-challenge.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 1,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "낱말이 없는 비용 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = unknown (expected cost)",
|
||||
"router = keyword (expected llm)",
|
||||
"refused = true (expected false)",
|
||||
"numbers_ok = null (expected true)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 없는 DLQ 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = unknown (expected dlq)",
|
||||
"router = keyword (expected llm)",
|
||||
"numbers_ok = null (expected true)",
|
||||
"answer has none of ['3']"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 없는 릴리스 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"area = unknown (expected release)",
|
||||
"router = keyword (expected llm)",
|
||||
"numbers_ok = null (expected true)",
|
||||
"answer has none of ['62.5']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "LLM도 운영 밖이면 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"router = keyword (expected llm)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 있으면 LLM을 부르지 않음",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 5 case(s) × 1
|
||||
✘ 낱말이 없는 비용 질문 · 0.0s · area = unknown (expected cost); router = keyword (expected llm); refused = true (expected false); numbers_ok = null (expected true)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✘ 낱말이 없는 DLQ 질문 · 0.1s · area = unknown (expected dlq); router = keyword (expected llm); numbers_ok = null (expected true); answer has none of ['3']
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✘ 낱말이 없는 릴리스 질문 · 0.0s · area = unknown (expected release); router = keyword (expected llm); numbers_ok = null (expected true); answer has none of ['62.5']
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✘ LLM도 운영 밖이면 거절 · 0.1s · router = keyword (expected llm)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 낱말이 있으면 LLM을 부르지 않음 · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 1/5
|
||||
report: runs/lab02-challenge-20261101-185310.json
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"cases": "lab02-challenge.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "낱말이 없는 비용 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "cost",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "이번 주 AI 사용 비용은 총 0.073604달러입니다. 일일 평균 비용은 약 0.08달러이며, 사용 비율은 92.01%입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 없는 DLQ 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "어젯밤에 접수 못 하고 남은 건은 총 3건입니다. 이 중 2건은 대기 중이고 1건은 주차 상태입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 없는 릴리스 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?"
|
||||
},
|
||||
"response": {
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?",
|
||||
"area": "release",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "새 판 1.2.0은 일부 사용자 20%에게 먼저 내보내졌으며, 이 버전의 재사용률은 62.5%로 나타났습니다. 현재 전체 시스템 상태는 빨간색(문제 있음)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "LLM도 운영 밖이면 거절",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "낱말이 있으면 LLM을 부르지 않음",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
$ uv run tools/flow.py push answers/79-lab02-challenge-llm-router.json
|
||||
! unpublished '79 Ops ask + refuse + number check (reference)' (w48289056bc6d55d): it used the same webhook path
|
||||
✔ pushed '79 Ops ask + LLM router (answer)' (id wea68a09a2860083)
|
||||
POST http://localhost:5678/webhook/my-ops-ask
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 5 case(s) × 1
|
||||
✔ 낱말이 없는 비용 질문 · 4.9s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "이번 주에 AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "cost",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "이번 주 AI 사용 비용은 총 0.073604달러입니다. 일일 평균 비용은 약 0.08달러이며, 사용 비율은 92.01%입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 낱말이 없는 DLQ 질문 · 1.7s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "어젯밤에 접수 못 하고 남은 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "어젯밤에 접수 못 하고 남은 건은 총 3건입니다. 이 중 2건은 대기 중이고 1건은 주차 상태입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 낱말이 없는 릴리스 질문 · 1.8s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "새 판을 일부 사용자에게 먼저 내보낸 거 어떻게 되고 있어?",
|
||||
"area": "release",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "새 판 1.2.0은 일부 사용자 20%에게 먼저 내보내졌으며, 이 버전의 재사용률은 62.5%로 나타났습니다. 현재 전체 시스템 상태는 빨간색(문제 있음)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ LLM도 운영 밖이면 거절 · 0.9s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 낱말이 있으면 LLM을 부르지 않음 · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab02-challenge-20261101-185333.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ 실행 · 오류율 · 1.0s
|
||||
✔ DLQ · parked · 1.1s
|
||||
✔ 운영과 상관없는 질문은 거절 · 0.9s
|
||||
✔ 작업 요청은 거절 (읽기만) · 0.0s
|
||||
✔ 비용 · 숫자 확인 · 1.0s
|
||||
✔ 릴리스 · 숫자 확인 · 1.4s
|
||||
Passed 6/6
|
||||
report: runs/lab02-20261101-185339.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-phrasing.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ '에러' → 실행 · 1.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "에러 많이 났어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "에러 많이 났어?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 총 357회 실행 중 21회 에러가 발생하여 에러율은 5.88%입니다. 주요 실패 원인은 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과로 16회 발생했습니다. 현재 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✘ '장애' 는 낱말 목록에 없음 → 운영 밖으로 거절 · 2.2s · area = runs (expected unknown); refused = false (expected true); reason = null (expected out_of_scope)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "장애 났어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "장애 났어?",
|
||||
"area": "runs",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "네, 2026년 10월 26일부터 30일까지 장애가 발생했습니다. 전체 357건 중 21건(5.88%)에서 오류가 있었고, 주요 장애는 'Search FAQ' 노드에서 1000ms 타임아웃 초과로 16건 발생했습니다. 시스템 상태는 빨간색(위험)입니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 두 영역이 한 번씩 → 앞의 영역 (알림) · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "알림이랑 DLQ 둘 다 알려 줘",
|
||||
"area": "alerts",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "알림은 총 6건이며, 이 중 3건은 높은 심각도입니다. DLQ 관련 정보는 제공된 사실에 포함되어 있지 않습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ '꺼 줘' → 작업 요청으로 거절 · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "카나리 꺼 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "카나리 꺼 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
✔ '배포'(1) 보다 '상태 · 요약'(2) → 전체 · 1.7s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "배포 상태 요약해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "배포 상태 요약해 줘",
|
||||
"area": "overview",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "배포 상태 요약입니다. 10월 26일부터 30일까지 총 357회 실행 중 오류율은 5.88%이며, 주요 오류는 'Search FAQ' 노드에서 1000ms 타임아웃이 16회 발생했습니다. 전체 상태는 'red'로, 실행, DLQ, 릴리스가 심각한 문제를 보이고 있습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✘ 낱말 없는 비용 질문 → 거절 (과제 1에서 LLM 라우터로) · 1.9s · refused = false (expected true); reason = null (expected out_of_scope)
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "AI 쓰는 데 얼마나 나갔어?",
|
||||
"area": "cost",
|
||||
"router": "llm",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "2026년 10월 26일부터 30일까지 AI 사용 비용은 총 0.0736달러였습니다. 하루 평균 비용은 약 0.08달러이며, 전체 사용량의 92.01%가 사용되었습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 4/6
|
||||
report: runs/lab02-phrasing-20261101-185347.json
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"cases": "lab02-challenge2.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 4,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "작업 요청",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "운영 밖",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "답한 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "감사 기록 세 줄 · 거절 이유와 함께",
|
||||
"webhook": "http://localhost:8800/audit/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"events.0.kind = null (expected ops-ask)",
|
||||
"events.0.reason = null (expected read_only)",
|
||||
"events.0.area = null (expected action)",
|
||||
"events.1.reason = null (expected out_of_scope)",
|
||||
"events.1.router = null (expected llm)",
|
||||
"events.2.area = null (expected runs)",
|
||||
"events.2.refused = null (expected false)",
|
||||
"events.2.numbers_ok = null (expected true)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"events": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "응답은 그대로 (감사는 곁가지)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 5 case(s) × 1
|
||||
✔ 작업 요청 · 0.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
✔ 운영 밖 · 0.7s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 답한 질문 · 1.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✘ 감사 기록 세 줄 · 거절 이유와 함께 · 0.0s · events.0.kind = null (expected ops-ask); events.0.reason = null (expected read_only); events.0.area = null (expected action); events.1.reason = null (expected out_of_scope); events.1.router = null (expected llm); events.2.area = null (expected runs); events.2.refused = null (expected false); events.2.numbers_ok = null (expected true)
|
||||
→ request POST http://localhost:8800/audit/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"events": []
|
||||
}
|
||||
✔ 응답은 그대로 (감사는 곁가지) · 0.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
Passed 4/5
|
||||
report: runs/lab02-challenge2-20261101-185349.json
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"cases": "lab02-challenge2.json",
|
||||
"webhook": "my-ops-ask",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "작업 요청",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "운영 밖",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.6,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "답한 질문",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
},
|
||||
"response": {
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "감사 기록 세 줄 · 거절 이유와 함께",
|
||||
"webhook": "http://localhost:8800/audit/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"events": [
|
||||
{
|
||||
"t": 1793526843.503,
|
||||
"kind": "ops-ask",
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"numbers_ok": null
|
||||
},
|
||||
{
|
||||
"t": 1793526845.149,
|
||||
"kind": "ops-ask",
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"numbers_ok": null
|
||||
},
|
||||
{
|
||||
"t": 1793526846.297,
|
||||
"kind": "ops-ask",
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"numbers_ok": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "응답은 그대로 (감사는 곁가지)",
|
||||
"webhook": "my-ops-ask",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
},
|
||||
"response": {
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
$ uv run tools/flow.py push answers/79-lab02-challenge2-audit.json
|
||||
! unpublished '79 Ops ask + LLM router (answer)' (wea68a09a2860083): it used the same webhook path
|
||||
✔ pushed '79 Ops ask + audit (answer)' (id w06fbfa6c9a05e92)
|
||||
POST http://localhost:5678/webhook/my-ops-ask
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 5 case(s) × 1
|
||||
✔ 작업 요청 · 1.0s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
✔ 운영 밖 · 1.6s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"answer": "운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다."
|
||||
}
|
||||
✔ 답한 질문 · 1.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"answer": "지난주 오류율은 5.88%였습니다. 총 357회 실행 중 21회 오류가 발생했습니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 감사 기록 세 줄 · 거절 이유와 함께 · 0.0s
|
||||
→ request POST http://localhost:8800/audit/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"t": 1793526843.503,
|
||||
"kind": "ops-ask",
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"numbers_ok": null
|
||||
},
|
||||
{
|
||||
"t": 1793526845.149,
|
||||
"kind": "ops-ask",
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"router": "llm",
|
||||
"refused": true,
|
||||
"reason": "out_of_scope",
|
||||
"numbers_ok": null
|
||||
},
|
||||
{
|
||||
"t": 1793526846.297,
|
||||
"kind": "ops-ask",
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"router": "keyword",
|
||||
"refused": false,
|
||||
"reason": null,
|
||||
"numbers_ok": true
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 응답은 그대로 (감사는 곁가지) · 0.1s
|
||||
→ request POST /webhook/my-ops-ask
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"router": "keyword",
|
||||
"refused": true,
|
||||
"reason": "read_only",
|
||||
"answer": "운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요."
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab02-challenge2-20261101-185406.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 6 case(s) × 1
|
||||
✔ 실행 · 오류율 · 1.0s
|
||||
✔ DLQ · parked · 1.2s
|
||||
✔ 운영과 상관없는 질문은 거절 · 0.8s
|
||||
✔ 작업 요청은 거절 (읽기만) · 0.1s
|
||||
✔ 비용 · 숫자 확인 · 1.2s
|
||||
✔ 릴리스 · 숫자 확인 · 1.6s
|
||||
Passed 6/6
|
||||
report: runs/lab02-20261101-185412.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-ask · 5 case(s) × 1
|
||||
✔ 낱말이 없는 비용 질문 · 2.3s
|
||||
✔ 낱말이 없는 DLQ 질문 · 1.7s
|
||||
✔ 낱말이 없는 릴리스 질문 · 1.6s
|
||||
✔ LLM도 운영 밖이면 거절 · 0.7s
|
||||
✔ 낱말이 있으면 LLM을 부르지 않음 · 1.2s
|
||||
Passed 5/5
|
||||
report: runs/lab02-challenge-20261101-185419.json
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"cases": "lab03.json",
|
||||
"webhook": "my-ops-brief",
|
||||
"passed": 0,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "전체 · 먼저 볼 것 셋",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"overall = null (expected red)",
|
||||
"red = null (expected [\"runs\", \"dlq\", \"release\"])",
|
||||
"actions.0.area = null (expected runs)",
|
||||
"actions.1.area = null (expected dlq)",
|
||||
"actions.2.area = null (expected release)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"markdown has none of ['| DLQ | 🔴 | 3건 (parked 2) |']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"markdown has none of ['카나리 끄기 · 되돌리기 검토 (74일차)']",
|
||||
"actions.1.action has none of ['parked']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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": "DLQ를 비운 뒤",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"red = null (expected [\"runs\", \"release\"])",
|
||||
"actions.2.area = null (expected alerts)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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,77 @@
|
||||
$ uv run tools/flow.py test cases/lab03.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 4 case(s) × 1
|
||||
✘ 전체 · 먼저 볼 것 셋 · 0.0s · status 404 (expected 200); overall = null (expected red); red = null (expected ["runs", "dlq", "release"]); actions.0.area = null (expected runs); actions.1.area = null (expected dlq); actions.2.area = null (expected release)
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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 ['| DLQ | 🔴 | 3건 (parked 2) |']
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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 ['카나리 끄기 · 되돌리기 검토 (74일차)']; actions.1.action has none of ['parked']
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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)"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 3
|
||||
}
|
||||
✘ DLQ를 비운 뒤 · 0.0s · status 404 (expected 200); red = null (expected ["runs", "release"]); actions.2.area = null (expected alerts)
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-brief\" 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/4
|
||||
report: runs/lab03-20261101-185420.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"cases": "lab03.json",
|
||||
"webhook": "my-ops-brief",
|
||||
"passed": 4,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "전체 · 먼저 볼 것 셋",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "표에 숫자",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.2,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "할 일은 규칙에서",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ를 비운 뒤",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟢 | 0건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n3. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
$ uv run tools/flow.py push reference/lab03-example1-brief.json
|
||||
✔ pushed '79 Ops brief (reference)' (id w97397012b52e601)
|
||||
POST http://localhost:5678/webhook/my-ops-brief
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 4 case(s) × 1
|
||||
✔ 전체 · 먼저 볼 것 셋 · 1.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
✔ 표에 숫자 · 0.2s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
✔ 할 일은 규칙에서 · 0.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 3
|
||||
}
|
||||
✔ DLQ를 비운 뒤 · 0.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟢 | 0건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n3. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
Passed 4/4
|
||||
report: runs/lab03-20261101-185435.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py call my-ops-brief '{}'
|
||||
HTTP 200 · 0.1s
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"cases": "lab03-llm.json",
|
||||
"webhook": "my-ops-brief",
|
||||
"passed": 0,
|
||||
"total": 2,
|
||||
"results": [
|
||||
{
|
||||
"case": "요약 세 문장 · 숫자 확인",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"numbers_ok = null (expected true)",
|
||||
"markdown has none of ['## 요약']"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "요약에 급한 곳",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"summary has none of ['DLQ', '릴리스', '카나리', '실행', '오류']"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
$ uv run tools/flow.py test cases/lab03-llm.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 2 case(s) × 1
|
||||
✘ 요약 세 문장 · 숫자 확인 · 0.1s · numbers_ok = null (expected true); markdown has none of ['## 요약']
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
✘ 요약에 급한 곳 · 0.1s · summary has none of ['DLQ', '릴리스', '카나리', '실행', '오류']
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
Passed 0/2
|
||||
report: runs/lab03-llm-20261101-185435.json
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"cases": "lab03-llm.json",
|
||||
"webhook": "my-ops-brief",
|
||||
"passed": 2,
|
||||
"total": 2,
|
||||
"results": [
|
||||
{
|
||||
"case": "요약 세 문장 · 숫자 확인",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.5,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 DLQ 크기는 3입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생하는 1000ms 타임아웃 오류로 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 DLQ 크기는 3입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생하는 1000ms 타임아웃 오류로 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "요약에 급한 곳",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
$ uv run tools/flow.py push reference/lab03-example2-brief.json
|
||||
! unpublished '79 Ops brief (reference)' (w97397012b52e601): it used the same webhook path
|
||||
✔ pushed '79 Ops brief + summary (reference)' (id w871e57398bd7b32)
|
||||
POST http://localhost:5678/webhook/my-ops-brief
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-llm.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 2 case(s) × 1
|
||||
✔ 요약 세 문장 · 숫자 확인 · 3.5s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 DLQ 크기는 3입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생하는 1000ms 타임아웃 오류로 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 DLQ 크기는 3입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생하는 1000ms 타임아웃 오류로 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 요약에 급한 곳 · 1.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 2/2
|
||||
report: runs/lab03-llm-20261101-185454.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 4 case(s) × 1
|
||||
✔ 전체 · 먼저 볼 것 셋 · 1.3s
|
||||
✔ 표에 숫자 · 1.1s
|
||||
✔ 할 일은 규칙에서 · 1.1s
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ DLQ를 비운 뒤 · 1.2s
|
||||
Passed 4/4
|
||||
report: runs/lab03-20261101-185459.json
|
||||
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"cases": "lab03-after.json",
|
||||
"webhook": "my-ops-brief",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "아침 그대로",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.4,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, DLQ 크기는 3으로 심각한 문제를 나타냅니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 발생하는 1000ms 타임아웃 오류로, 총 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중하여 시스템 안정성을 확보해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, DLQ 크기는 3으로 심각한 문제를 나타냅니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 발생하는 1000ms 타임아웃 오류로, 총 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중하여 시스템 안정성을 확보해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "parked 둘을 처리하면 → DLQ는 노랑, 먼저 볼 것에 알림",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n3. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "카나리를 끄면 → 릴리스는 초록",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예산을 올리면 → 비용도 초록",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟢 | $0.073604 / $0.2 (36.8%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과가 16건 발생한 것입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과가 16건 발생한 것입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "남은 빨강은 실행 하나 (사람이 고칠 일)",
|
||||
"webhook": "my-ops-brief",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟢 | $0.073604 / $0.2 (36.8%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
$ uv run tools/flow.py test cases/lab03-after.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-brief · 5 case(s) × 1
|
||||
✔ 아침 그대로 · 1.4s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"dlq",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "red",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🔴 | 3건 (parked 2) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n3. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, DLQ 크기는 3으로 심각한 문제를 나타냅니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 발생하는 1000ms 타임아웃 오류로, 총 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중하여 시스템 안정성을 확보해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며, DLQ 크기는 3으로 심각한 문제를 나타냅니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 발생하는 1000ms 타임아웃 오류로, 총 16건이 보고되었습니다. 우선 이 타임아웃 문제를 해결하는 데 집중하여 시스템 안정성을 확보해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 1,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 1}
|
||||
→ request POST http://localhost:8800/dlq/update
|
||||
{
|
||||
"dlq_id": 2,
|
||||
"remove": true
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"removed": true,
|
||||
"size": 1
|
||||
}
|
||||
✔ parked 둘을 처리하면 → DLQ는 노랑, 먼저 볼 것에 알림 · 1.0s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs",
|
||||
"release"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "release",
|
||||
"status": "red",
|
||||
"action": "카나리 끄기 · 되돌리기 검토 (74일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🔴 | 1.1.0 · 카나리 1.2.0 20% (다시 묻기 62.5%) |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🔴 릴리스: 카나리 끄기 · 되돌리기 검토 (74일차)\n3. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ before: POST http://localhost:8800/releases/helpdesk · HTTP 200 · 0.0s · {"name": "helpdesk", "active": "1.1.0", "canary": null, "require_gate": false, "releases": {"1.0.0": {"prompt_version":
|
||||
→ request POST http://localhost:8800/releases/helpdesk
|
||||
{
|
||||
"canary": null
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"name": "helpdesk",
|
||||
"active": "1.1.0",
|
||||
"canary": null,
|
||||
"require_gate": false,
|
||||
"releases": {
|
||||
"1.0.0": {
|
||||
"prompt_version": "p1",
|
||||
"model": "gpt-4.1-mini",
|
||||
"note": "첫 릴리스"
|
||||
},
|
||||
"1.1.0": {
|
||||
"prompt_version": "p2",
|
||||
"model": "gpt-4.1-mini",
|
||||
"note": "FAQ 답 끝에 출처 번호"
|
||||
},
|
||||
"1.2.0": {
|
||||
"prompt_version": "p3",
|
||||
"model": "gpt-4.1-mini",
|
||||
"note": "답을 한 문장으로 짧게"
|
||||
},
|
||||
"2.0.0": {
|
||||
"prompt_version": "p2",
|
||||
"model": "gpt-5.4-nano",
|
||||
"note": "더 작은 모델 (비용)"
|
||||
},
|
||||
"2.1.0": {
|
||||
"prompt_version": "p9",
|
||||
"model": "gpt-4.1-mini",
|
||||
"note": "출처 번호 · 존댓말 다듬기 (지시문 이름 확인 필요)"
|
||||
}
|
||||
}
|
||||
}
|
||||
✔ 카나리를 끄면 → 릴리스는 초록 · 1.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality",
|
||||
"cost"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟡 | $0.073604 / $0.08 (92.01%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.2, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.073604, "remaining_usd": 0.126396,
|
||||
→ request POST http://localhost:8800/budget
|
||||
{
|
||||
"daily_usd": 0.2
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"daily_usd": 0.2,
|
||||
"user_limit_per_min": 20,
|
||||
"max_question_chars": 500,
|
||||
"used_usd": 0.073604,
|
||||
"remaining_usd": 0.126396,
|
||||
"over": false
|
||||
}
|
||||
✔ 예산을 올리면 → 비용도 초록 · 1.1s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟢 | $0.073604 / $0.2 (36.8%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과가 16건 발생한 것입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%이며 응답하지 않은 비율은 4.76%입니다. 가장 긴급한 문제는 \"Search FAQ\" 노드에서 1000ms 타임아웃 초과가 16건 발생한 것입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
✔ 남은 빨강은 실행 하나 (사람이 고칠 일) · 1.2s
|
||||
→ request POST /webhook/my-ops-brief
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"overall": "red",
|
||||
"red": [
|
||||
"runs"
|
||||
],
|
||||
"yellow": [
|
||||
"alerts",
|
||||
"dlq",
|
||||
"quality"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"area": "runs",
|
||||
"status": "red",
|
||||
"action": "오류 묶음 1위 확인 · 고치기 (77일차)"
|
||||
},
|
||||
{
|
||||
"area": "alerts",
|
||||
"status": "yellow",
|
||||
"action": "높은 알림부터 확인하고 닫기 (76일차)"
|
||||
},
|
||||
{
|
||||
"area": "dlq",
|
||||
"status": "yellow",
|
||||
"action": "parked 건 원인 확인 뒤 런북대로 재처리 (76일차)"
|
||||
}
|
||||
],
|
||||
"markdown": "# 운영 브리핑 (2026-10-26 ~ 2026-10-30 실행 기록 + 지금 상태)\n\n전체: 🔴 red\n\n| 영역 | 상태 | 숫자 |\n| --- | --- | --- |\n| 실행 | 🔴 | 오류율 5.88% · p95 2598ms · 답 못 함 4.76% |\n| 알림 | 🟡 | 6건 (높음 3) |\n| DLQ | 🟡 | 1건 (parked 0) |\n| 데이터 품질 | 🟡 | 평균 0.6583 · 마지막 1 · 격리 1 |\n| 비용 | 🟢 | $0.073604 / $0.2 (36.8%) |\n| 릴리스 | 🟢 | 1.1.0 |\n\n## 먼저 볼 것\n\n1. 🔴 실행: 오류 묶음 1위 확인 · 고치기 (77일차)\n2. 🟡 알림: 높은 알림부터 확인하고 닫기 (76일차)\n3. 🟡 DLQ: parked 건 원인 확인 뒤 런북대로 재처리 (76일차)\n\n## 요약\n\n전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"summary": "전체 시스템 상태는 빨간색으로, 오류율은 5.88%로 나타났습니다. 가장 긴급한 문제는 'Search FAQ' 노드에서 발생한 16건의 타임아웃 오류입니다. 우선 이 타임아웃 문제를 해결하는 데 집중해야 합니다.",
|
||||
"numbers_ok": true,
|
||||
"unknown_numbers": []
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab03-after-20261101-185505.json
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"cases": "lab03-challenge.json",
|
||||
"webhook": "my-ops-eval",
|
||||
"passed": 0,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "골든 질문 8개",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"total = null (expected 8)",
|
||||
"area_ok = null (expected 8)",
|
||||
"refused_ok = null (expected 8)",
|
||||
"score = null (expected 0.875..1)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"total = null (expected 1)",
|
||||
"passed = null (expected 0)",
|
||||
"failed.0.area = null (expected alerts)",
|
||||
"failed.0.area_ok = null (expected false)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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": "빈 목록 → 400",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 400)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {
|
||||
"cases": []
|
||||
},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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,115 @@
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-eval · 3 case(s) × 1
|
||||
✘ 골든 질문 8개 · 0.0s · status 404 (expected 200); total = null (expected 8); area_ok = null (expected 8); refused_ok = null (expected 8); score = null (expected 0.875..1)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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); total = null (expected 1); passed = null (expected 0); failed.0.area = null (expected alerts); failed.0.area_ok = null (expected false)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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)"
|
||||
}
|
||||
✘ 빈 목록 → 400 · 0.0s · status 404 (expected 400)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": []
|
||||
}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-eval\" 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/lab03-challenge-20261101-185505.json
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"cases": "lab03-challenge.json",
|
||||
"webhook": "my-ops-eval",
|
||||
"passed": 3,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "골든 질문 8개",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "틀린 기대는 잡아냄",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 있습니다. 이 중 3건은 높은 심각도 알림입니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "빈 목록 → 400",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 400,
|
||||
"request": {
|
||||
"cases": []
|
||||
},
|
||||
"response": {
|
||||
"error": "cases must be a non-empty list"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
$ uv run tools/flow.py push answers/79-lab03-challenge-eval.json
|
||||
! unpublished '79 Ops assistant eval + gate (answer)' (wb750a2ccc06d9a1): it used the same webhook path
|
||||
✔ pushed '79 Ops assistant eval (answer)' (id w7803df55a337611)
|
||||
POST http://localhost:5678/webhook/my-ops-eval
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-eval · 3 case(s) × 1
|
||||
✔ 골든 질문 8개 · 3.9s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": []
|
||||
}
|
||||
✔ 틀린 기대는 잡아냄 · 1.0s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 있습니다. 이 중 3건은 높은 심각도 알림입니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 빈 목록 → 400 · 0.0s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": []
|
||||
}
|
||||
← response HTTP 400
|
||||
{
|
||||
"error": "cases must be a non-empty list"
|
||||
}
|
||||
Passed 3/3
|
||||
report: runs/lab03-challenge-20261101-185725.json
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"cases": "lab03-challenge2.json",
|
||||
"webhook": "my-ops-eval",
|
||||
"passed": 0,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "기준을 안 주면 0.9",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"min_score = null (expected 0.9)",
|
||||
"gate = null (expected pass)"
|
||||
],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"score": 1,
|
||||
"area_ok": 1,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "골든 8 · 기준 0.8 → 통과",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"gate = null (expected pass)",
|
||||
"min_score = null (expected 0.8)"
|
||||
],
|
||||
"seconds": 2.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
],
|
||||
"min_score": 0.8
|
||||
},
|
||||
"response": {
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "틀린 기대 · 기준 0.9 → 실패",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"gate = null (expected fail)"
|
||||
],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"min_score": 0.9
|
||||
},
|
||||
"response": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 와 있으며, 이 중 3건은 높은 심각도입니다. 알림 사유로는 타임아웃 1건, 속도 제한 1건, 배치 거부 1건, 저품질 2건, API 오류 1건이 있습니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "실패하면 알림 한 건",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 0 (expected 1)",
|
||||
"alerts.0.workflow = null (expected ops-eval)",
|
||||
"alerts.0.score = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"reason": "eval_below"
|
||||
},
|
||||
"response": {
|
||||
"count": 0,
|
||||
"alerts": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-eval · 4 case(s) × 1
|
||||
✘ 기준을 안 주면 0.9 · 1.2s · min_score = null (expected 0.9); gate = null (expected pass)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"score": 1,
|
||||
"area_ok": 1,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": []
|
||||
}
|
||||
✘ 골든 8 · 기준 0.8 → 통과 · 2.7s · gate = null (expected pass); min_score = null (expected 0.8)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
],
|
||||
"min_score": 0.8
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": []
|
||||
}
|
||||
✘ 틀린 기대 · 기준 0.9 → 실패 · 1.2s · gate = null (expected fail)
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"min_score": 0.9
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 와 있으며, 이 중 3건은 높은 심각도입니다. 알림 사유로는 타임아웃 1건, 속도 제한 1건, 배치 거부 1건, 저품질 2건, API 오류 1건이 있습니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 실패하면 알림 한 건 · 0.0s · count = 0 (expected 1); alerts.0.workflow = null (expected ops-eval); alerts.0.score = null (expected 0)
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{
|
||||
"reason": "eval_below"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 0,
|
||||
"alerts": []
|
||||
}
|
||||
Passed 0/4
|
||||
report: runs/lab03-challenge2-20261101-185730.json
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"cases": "lab03-challenge2.json",
|
||||
"webhook": "my-ops-eval",
|
||||
"passed": 4,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "기준을 안 주면 0.9",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.3,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"score": 1,
|
||||
"area_ok": 1,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [],
|
||||
"min_score": 0.9,
|
||||
"gate": "pass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "골든 8 · 기준 0.8 → 통과",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
],
|
||||
"min_score": 0.8
|
||||
},
|
||||
"response": {
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": [],
|
||||
"min_score": 0.8,
|
||||
"gate": "pass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "틀린 기대 · 기준 0.9 → 실패",
|
||||
"webhook": "my-ops-eval",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"min_score": 0.9
|
||||
},
|
||||
"response": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 와 있으며, 이 중 3건은 높은 심각도입니다. 알림 사유로는 타임아웃 1건, 속도 제한 1건, 배치 거부 1건, 저품질 2건, API 오류 1건이 있습니다."
|
||||
}
|
||||
],
|
||||
"min_score": 0.9,
|
||||
"gate": "fail"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "실패하면 알림 한 건",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"reason": "eval_below"
|
||||
},
|
||||
"response": {
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "ops-eval",
|
||||
"node": "Below?",
|
||||
"severity": "medium",
|
||||
"reason": "eval_below",
|
||||
"score": 0,
|
||||
"min_score": 0.9,
|
||||
"message": "ops assistant eval 0 < 0.9",
|
||||
"at": 1793527072.438837
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
$ uv run tools/flow.py push answers/79-lab03-challenge2-gate.json
|
||||
! unpublished '79 Ops assistant eval (answer)' (w7803df55a337611): it used the same webhook path
|
||||
✔ pushed '79 Ops assistant eval + gate (answer)' (id wb750a2ccc06d9a1)
|
||||
POST http://localhost:5678/webhook/my-ops-eval
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
→ request POST http://localhost:8800/ops-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"seeded_at": "2026-11-04T08:30:00",
|
||||
"note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
|
||||
"counts": {
|
||||
"alerts": 6,
|
||||
"dlq": 3,
|
||||
"quality_log": 4,
|
||||
"quarantine": 1,
|
||||
"usage": 192
|
||||
}
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-eval · 4 case(s) × 1
|
||||
✔ 기준을 안 주면 0.9 · 3.3s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"score": 1,
|
||||
"area_ok": 1,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [],
|
||||
"min_score": 0.9,
|
||||
"gate": "pass"
|
||||
}
|
||||
✔ 골든 8 · 기준 0.8 → 통과 · 3.4s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "지난주 오류율이 얼마야?",
|
||||
"area": "runs",
|
||||
"numbers": [
|
||||
"5.88"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ에 parked 된 건 몇 건이야?",
|
||||
"area": "dlq",
|
||||
"numbers": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "FAQ 배치 품질 점수 평균은?",
|
||||
"area": "quality",
|
||||
"numbers": [
|
||||
"0.6583",
|
||||
"65.83",
|
||||
"0.66"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "오늘 LLM 비용이 예산의 몇 퍼센트야?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"92"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "카나리 버전 문제 있어?",
|
||||
"area": "release",
|
||||
"numbers": [
|
||||
"62.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "DLQ 1번 다시 돌려 줘",
|
||||
"area": "action",
|
||||
"refused": true
|
||||
},
|
||||
{
|
||||
"question": "점심 메뉴 추천해 줘",
|
||||
"area": "unknown",
|
||||
"refused": true
|
||||
}
|
||||
],
|
||||
"min_score": 0.8
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 8,
|
||||
"passed": 8,
|
||||
"score": 1,
|
||||
"area_ok": 8,
|
||||
"refused_ok": 8,
|
||||
"number_ok": 8,
|
||||
"grounded": 8,
|
||||
"failed": [],
|
||||
"min_score": 0.8,
|
||||
"gate": "pass"
|
||||
}
|
||||
✔ 틀린 기대 · 기준 0.9 → 실패 · 1.2s
|
||||
→ request POST /webhook/my-ops-eval
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "cost",
|
||||
"numbers": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"min_score": 0.9
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"score": 0,
|
||||
"area_ok": 0,
|
||||
"refused_ok": 1,
|
||||
"number_ok": 1,
|
||||
"grounded": 1,
|
||||
"failed": [
|
||||
{
|
||||
"question": "알림이 몇 건 와 있어?",
|
||||
"area": "alerts",
|
||||
"ok": false,
|
||||
"area_ok": false,
|
||||
"refused_ok": true,
|
||||
"number_ok": true,
|
||||
"grounded": true,
|
||||
"answer": "알림은 총 6건이 와 있으며, 이 중 3건은 높은 심각도입니다. 알림 사유로는 타임아웃 1건, 속도 제한 1건, 배치 거부 1건, 저품질 2건, API 오류 1건이 있습니다."
|
||||
}
|
||||
],
|
||||
"min_score": 0.9,
|
||||
"gate": "fail"
|
||||
}
|
||||
✔ 실패하면 알림 한 건 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{
|
||||
"reason": "eval_below"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "ops-eval",
|
||||
"node": "Below?",
|
||||
"severity": "medium",
|
||||
"reason": "eval_below",
|
||||
"score": 0,
|
||||
"min_score": 0.9,
|
||||
"message": "ops assistant eval 0 < 0.9",
|
||||
"at": 1793527072.438837
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 4/4
|
||||
report: runs/lab03-challenge2-20261101-185752.json
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json
|
||||
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
|
||||
POST http://localhost:5678/webhook/my-ops-eval · 3 case(s) × 1
|
||||
✔ 골든 질문 8개 · 2.2s
|
||||
✔ 틀린 기대는 잡아냄 · 1.1s
|
||||
✔ 빈 목록 → 400 · 0.0s
|
||||
Passed 3/3
|
||||
report: runs/lab03-challenge-20261101-185755.json
|
||||
|
||||
$ uv run tools/flow.py morning
|
||||
seeded morning 2026-11-04T08:30:00 · {"alerts": 7, "dlq": 3, "quality_log": 4, "quarantine": 1, "usage": 192}
|
||||
alerts: 7 (high 3)
|
||||
[high] helpdesk/Answer · rate_limit · 429 Rate limit reached for requests
|
||||
[high] faq-import/Batch ok? · batch_rejected · B-1 rejected
|
||||
[medium] faq-import/Low score? · low_quality · B-2 quality 0.8
|
||||
[medium] faq-import/Low score? · low_quality · B-3 quality 0.8333
|
||||
[medium] intake/Create ticket · api_error · tickets API 503
|
||||
[medium] ops-eval/Below? · eval_below · ops assistant eval 0 < 0.9
|
||||
DLQ: 3 (parked 2)
|
||||
#1 attempts 3 parked True tickets API 503
|
||||
#2 attempts 3 parked True tickets API 503
|
||||
#3 attempts 1 parked False timeout
|
||||
quality log: 4 (scores [0, 0.8, 0.8333, 1]) · quarantine 1
|
||||
usage: 156 requests · 192 calls · $0.073604
|
||||
budget: used $0.073604 of $0.08
|
||||
release: active 1.1.0 · canary {"release": "1.2.0", "percent": 20}
|
||||
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ back to the seeded operations morning (alerts · DLQ · quality · usage · budget · release), live logs · added FAQ · tickets 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)
|
||||
✔ schemas faq · employee · ticket, quarantine, quality log (day 78 data API)
|
||||
✔ operations morning /ops/seed-info (day 79 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,281 @@
|
||||
{
|
||||
"name": "79 Ops facts + health (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Facts webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-facts",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "663cb969-bc5a-52e7-929e-7e0161c2806d",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "9bcf9913-ebc6-5ee9-af9d-284d07aa672e"
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"options": {},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $('Facts webhook').first().json.body?.from || '2026-10-26', to: $('Facts webhook').first().json.body?.to || '2026-10-30' }) }}"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "2901463b-a9cd-5a60-a507-ee5d8c7e427b"
|
||||
},
|
||||
{
|
||||
"name": "Get alerts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "83c33a5b-211d-5df2-812f-213bb1475e0d"
|
||||
},
|
||||
{
|
||||
"name": "Get DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "d4d021cb-61a5-57c7-acee-442d5a61c08a"
|
||||
},
|
||||
{
|
||||
"name": "Get quality",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/quality-log",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "868d3302-7097-5d73-bf80-23bd45e6872e"
|
||||
},
|
||||
{
|
||||
"name": "Get quarantine",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/quarantine",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "d6269b34-e578-5ee4-b7fe-3db2a9caead9"
|
||||
},
|
||||
{
|
||||
"name": "Get usage",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/usage/summary",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "8b616d0e-c4ae-586a-bde6-5030bf878fb7"
|
||||
},
|
||||
{
|
||||
"name": "Get budget",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/budget",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "9b25971c-75c1-5f81-8aa2-6e186fa1aa98"
|
||||
},
|
||||
{
|
||||
"name": "Get release",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/releases/helpdesk",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "cf9221dc-d0d7-5b8a-8a59-027c2ea4a040"
|
||||
},
|
||||
{
|
||||
"name": "Make facts",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Facts webhook').first().json.body ?? {};\nconst from = body.from || '2026-10-26', to = body.to || '2026-10-30';\nconst r4 = (v) => Math.round(v * 10000) / 10000;\nconst logs = $('Get logs').first().json.logs ?? [];\nconst total = logs.length, errors = logs.filter((l) => l.status === 'error').length;\nconst d = logs.map((l) => l.duration_ms).sort((a, b) => a - b);\nconst p95 = d.length ? d[Math.max(0, Math.ceil(0.95 * d.length) - 1)] : 0;\nconst groups = {};\nfor (const l of logs.filter((x) => x.status === 'error')) { const k = l.error_node + '\\u0000' + l.error_message; groups[k] = (groups[k] ?? 0) + 1; }\nconst top = Object.entries(groups).sort((a, b) => b[1] - a[1])[0];\nconst rel = {};\nfor (const l of logs.filter((x) => x.route === '자동 답변')) { const x = (rel[l.release] ??= { answers: 0, repeats: 0 }); x.answers += 1; if (l.repeat) x.repeats += 1; }\nconst rate = (name) => rel[name] ? r4(rel[name].repeats / rel[name].answers) : null;\nconst alerts = $('Get alerts').first().json.alerts ?? [];\nconst byReason = {};\nfor (const a of alerts) byReason[a.reason ?? 'unknown'] = (byReason[a.reason ?? 'unknown'] ?? 0) + 1;\nconst dlq = $('Get DLQ').first().json.items ?? [];\nconst q = $('Get quality').first().json.items ?? [];\nconst quarantine = $('Get quarantine').first().json.size ?? 0;\nconst usage = $('Get usage').first().json, budget = $('Get budget').first().json;\nconst users = Object.entries(usage.by_user ?? {}).sort((a, b) => b[1].cost_usd - a[1].cost_usd);\nconst release = $('Get release').first().json;\nconst facts = {\n period: { from, to },\n runs: { total, errors, error_rate: total ? r4(errors / total) : 0, p95_ms: p95,\n unanswered_rate: total ? r4(logs.filter((l) => l.route === '담당자 연결').length / total) : 0,\n top_failure: top ? { node: top[0].split('\\u0000')[0], message: top[0].split('\\u0000')[1], count: top[1] } : null },\n alerts: { count: alerts.length, high: alerts.filter((a) => a.severity === 'high').length, by_reason: byReason },\n dlq: { size: dlq.length, parked: dlq.filter((x) => x.parked).length, waiting: dlq.filter((x) => !x.parked).length },\n quality: { batches: q.length, avg_score: q.length ? r4(q.reduce((s, x) => s + x.score, 0) / q.length) : 0,\n last_score: q.length ? q.at(-1).score : null, rejected_batches: q.filter((x) => x.rejected).length, quarantine },\n cost: { requests: usage.requests ?? 0, calls: usage.calls ?? 0, cost_usd: Math.round((usage.cost_usd ?? 0) * 1e6) / 1e6,\n daily_usd: budget.daily_usd, used_ratio: budget.daily_usd ? r4(budget.used_usd / budget.daily_usd) : 0,\n top_user: users.length ? { user: users[0][0], cost_usd: Math.round(users[0][1].cost_usd * 1e6) / 1e6, share: r4(users[0][1].cost_usd / usage.cost_usd) } : null },\n release: { active: release.active, canary_release: release.canary?.release ?? null, canary_percent: release.canary?.percent ?? 0,\n active_repeat_rate: rate(release.active), canary_repeat_rate: release.canary ? rate(release.canary.release) : null },\n};\nconst s = (red, yellow) => (red ? 'red' : yellow ? 'yellow' : 'green');\nconst f = facts;\nconst canaryBad = f.release.canary_release && f.release.canary_repeat_rate !== null && f.release.active_repeat_rate !== null\n && f.release.canary_repeat_rate > 2 * f.release.active_repeat_rate;\nconst health = {\n runs: s(f.runs.error_rate > 0.01, f.runs.unanswered_rate > 0.02),\n alerts: s(f.alerts.high >= 5, f.alerts.count >= 1),\n dlq: s(f.dlq.parked > 0, f.dlq.size > 0),\n quality: s(f.quality.last_score !== null && f.quality.last_score < 0.8, (f.quality.last_score !== null && f.quality.last_score < 0.9) || f.quality.quarantine > 0),\n cost: s(f.cost.used_ratio >= 1, f.cost.used_ratio >= 0.8),\n release: s(canaryBad, Boolean(f.release.canary_release)),\n};\nconst order = ['runs', 'alerts', 'dlq', 'quality', 'cost', 'release'];\nfacts.health = health;\nfacts.red = order.filter((k) => health[k] === 'red');\nfacts.yellow = order.filter((k) => health[k] === 'yellow');\nfacts.overall = facts.red.length ? 'red' : facts.yellow.length ? 'yellow' : 'green';\nreturn [{ json: facts }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
],
|
||||
"id": "6ea60af4-425b-593d-b50e-d6219a4cb3e0"
|
||||
},
|
||||
{
|
||||
"name": "Send facts",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
],
|
||||
"id": "e38da5f3-bc63-5f44-b2dd-4689706c462c"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Facts webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get alerts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get alerts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get quality",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get quality": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get quarantine",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get quarantine": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get usage",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get usage": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get budget",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get budget": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get release",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get release": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Make facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Make facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"name": "79 Ops facts + sources down (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Facts webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-facts",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "663cb969-bc5a-52e7-929e-7e0161c2806d",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "9bcf9913-ebc6-5ee9-af9d-284d07aa672e"
|
||||
},
|
||||
{
|
||||
"name": "Get logs",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/logs/query",
|
||||
"options": {},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $('Facts webhook').first().json.body?.from || '2026-10-26', to: $('Facts webhook').first().json.body?.to || '2026-10-30' }) }}"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "2901463b-a9cd-5a60-a507-ee5d8c7e427b"
|
||||
},
|
||||
{
|
||||
"name": "Get alerts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "83c33a5b-211d-5df2-812f-213bb1475e0d"
|
||||
},
|
||||
{
|
||||
"name": "Get DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "d4d021cb-61a5-57c7-acee-442d5a61c08a"
|
||||
},
|
||||
{
|
||||
"name": "Get quality",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/quality-log",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "868d3302-7097-5d73-bf80-23bd45e6872e"
|
||||
},
|
||||
{
|
||||
"name": "Get quarantine",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/quarantine",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "d6269b34-e578-5ee4-b7fe-3db2a9caead9"
|
||||
},
|
||||
{
|
||||
"name": "Get usage",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/usage/summary",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "8b616d0e-c4ae-586a-bde6-5030bf878fb7"
|
||||
},
|
||||
{
|
||||
"name": "Get budget",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/budget",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "9b25971c-75c1-5f81-8aa2-6e186fa1aa98"
|
||||
},
|
||||
{
|
||||
"name": "Get release",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/releases/helpdesk",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "cf9221dc-d0d7-5b8a-8a59-027c2ea4a040"
|
||||
},
|
||||
{
|
||||
"name": "Make facts",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const body = $('Facts webhook').first().json.body ?? {};\nconst from = body.from || '2026-10-26', to = body.to || '2026-10-30';\nconst r4 = (v) => Math.round(v * 10000) / 10000;\nconst logs = $('Get logs').first().json.logs ?? [];\nconst total = logs.length, errors = logs.filter((l) => l.status === 'error').length;\nconst d = logs.map((l) => l.duration_ms).sort((a, b) => a - b);\nconst p95 = d.length ? d[Math.max(0, Math.ceil(0.95 * d.length) - 1)] : 0;\nconst groups = {};\nfor (const l of logs.filter((x) => x.status === 'error')) { const k = l.error_node + '\\u0000' + l.error_message; groups[k] = (groups[k] ?? 0) + 1; }\nconst top = Object.entries(groups).sort((a, b) => b[1] - a[1])[0];\nconst rel = {};\nfor (const l of logs.filter((x) => x.route === '자동 답변')) { const x = (rel[l.release] ??= { answers: 0, repeats: 0 }); x.answers += 1; if (l.repeat) x.repeats += 1; }\nconst rate = (name) => rel[name] ? r4(rel[name].repeats / rel[name].answers) : null;\nconst alerts = $('Get alerts').first().json.alerts ?? [];\nconst byReason = {};\nfor (const a of alerts) byReason[a.reason ?? 'unknown'] = (byReason[a.reason ?? 'unknown'] ?? 0) + 1;\nconst dlq = $('Get DLQ').first().json.items ?? [];\nconst q = $('Get quality').first().json.items ?? [];\nconst quarantine = $('Get quarantine').first().json.size ?? 0;\nconst usage = $('Get usage').first().json, budget = $('Get budget').first().json;\nconst down = [];\nif (!Array.isArray($('Get alerts').first().json.alerts)) down.push('alerts');\nif (!Array.isArray($('Get DLQ').first().json.items)) down.push('dlq');\nif (!Array.isArray($('Get quality').first().json.items) || typeof $('Get quarantine').first().json.size !== 'number') down.push('quality');\nif (typeof usage.cost_usd !== 'number' || typeof budget.daily_usd !== 'number') down.push('cost');\nif (!('active' in ($('Get release').first().json ?? {}))) down.push('release');\nconst users = Object.entries(usage.by_user ?? {}).sort((a, b) => b[1].cost_usd - a[1].cost_usd);\nconst release = $('Get release').first().json;\nconst facts = {\n period: { from, to },\n runs: { total, errors, error_rate: total ? r4(errors / total) : 0, p95_ms: p95,\n unanswered_rate: total ? r4(logs.filter((l) => l.route === '담당자 연결').length / total) : 0,\n top_failure: top ? { node: top[0].split('\\u0000')[0], message: top[0].split('\\u0000')[1], count: top[1] } : null },\n alerts: { count: alerts.length, high: alerts.filter((a) => a.severity === 'high').length, by_reason: byReason },\n dlq: { size: dlq.length, parked: dlq.filter((x) => x.parked).length, waiting: dlq.filter((x) => !x.parked).length },\n quality: { batches: q.length, avg_score: q.length ? r4(q.reduce((s, x) => s + x.score, 0) / q.length) : 0,\n last_score: q.length ? q.at(-1).score : null, rejected_batches: q.filter((x) => x.rejected).length, quarantine },\n cost: { requests: usage.requests ?? 0, calls: usage.calls ?? 0, cost_usd: Math.round((usage.cost_usd ?? 0) * 1e6) / 1e6,\n daily_usd: budget.daily_usd, used_ratio: budget.daily_usd ? r4(budget.used_usd / budget.daily_usd) : 0,\n top_user: users.length ? { user: users[0][0], cost_usd: Math.round(users[0][1].cost_usd * 1e6) / 1e6, share: r4(users[0][1].cost_usd / usage.cost_usd) } : null },\n release: { active: release.active, canary_release: release.canary?.release ?? null, canary_percent: release.canary?.percent ?? 0,\n active_repeat_rate: rate(release.active), canary_repeat_rate: release.canary ? rate(release.canary.release) : null },\n};\nfor (const k of down) facts[k] = null;\nfacts.sources_down = down;\nconst s = (red, yellow) => (red ? 'red' : yellow ? 'yellow' : 'green');\nconst f = { ...facts, ...Object.fromEntries(down.map((k) => [k, {}])) };\nconst canaryBad = Boolean(f.release?.canary_release) && f.release.canary_repeat_rate !== null && f.release.active_repeat_rate !== null\n && f.release.canary_repeat_rate > 2 * f.release.active_repeat_rate;\nconst health0 = {\n runs: s(f.runs.error_rate > 0.01, f.runs.unanswered_rate > 0.02),\n alerts: s(f.alerts.high >= 5, f.alerts.count >= 1),\n dlq: s(f.dlq.parked > 0, f.dlq.size > 0),\n quality: s(f.quality.last_score !== null && f.quality.last_score < 0.8, (f.quality.last_score !== null && f.quality.last_score < 0.9) || f.quality.quarantine > 0),\n cost: s(f.cost.used_ratio >= 1, f.cost.used_ratio >= 0.8),\n release: s(canaryBad, Boolean(f.release.canary_release)),\n};\nconst order = ['runs', 'alerts', 'dlq', 'quality', 'cost', 'release'];\nconst health = Object.fromEntries(order.map((k) => [k, down.includes(k) ? 'unknown' : health0[k]]));\nfacts.health = health;\nfacts.red = order.filter((k) => health[k] === 'red');\nfacts.yellow = order.filter((k) => health[k] === 'yellow');\nfacts.unknown = order.filter((k) => health[k] === 'unknown');\nfacts.overall = facts.red.length ? 'red' : (facts.yellow.length || facts.unknown.length) ? 'yellow' : 'green';\nreturn [{ json: facts }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
],
|
||||
"id": "6ea60af4-425b-593d-b50e-d6219a4cb3e0"
|
||||
},
|
||||
{
|
||||
"name": "Send facts",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
],
|
||||
"id": "e38da5f3-bc63-5f44-b2dd-4689706c462c"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Facts webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get logs",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get logs": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get alerts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get alerts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get quality",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get quality": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get quarantine",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get quarantine": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get usage",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get usage": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get budget",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get budget": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get release",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get release": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Make facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Make facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
{
|
||||
"name": "79 Ops ask + LLM router (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Ask webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-ask",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "e0cc9a1d-38cf-580a-b52e-3f6d8bb1cab2",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "a2d5e198-0074-5f7f-9cd6-5203c98b0bb1"
|
||||
},
|
||||
{
|
||||
"name": "Route",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const q = String($('Ask webhook').first().json.body?.question ?? '').trim();\nconst lower = q.toLowerCase();\nconst words = {\n runs: ['오류', '에러', '실패율', '느려', '느린', '응답 시간', 'p95', '실행'],\n alerts: ['알림', '경보'],\n dlq: ['dlq', '재처리', '쌓인', 'parked', '접수 실패'],\n quality: ['품질', '배치', '격리'],\n cost: ['비용', '돈', '예산', '토큰', '요금'],\n release: ['릴리스', '버전', '카나리', '배포'],\n overview: ['괜찮', '상태', '요약', '전체', '브리핑'],\n};\nconst action = /(다시 돌려|재처리해|지워|삭제해|꺼 ?줘|켜 ?줘|배포해|되돌려|롤백해|바꿔 ?줘|올려 ?줘|내려 ?줘|비워 ?줘)/.test(q);\nif (action) return [{ json: { question: q, area: 'action', router: 'keyword' } }];\nlet area = 'unknown', best = 0;\nfor (const [k, list] of Object.entries(words)) {\n const n = list.filter((w) => lower.includes(w)).length;\n if (n > best) { best = n; area = k; }\n}\nreturn [{ json: { question: q, area, router: 'keyword' } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "a5744b67-3600-5a8a-8607-d6c0896313f1"
|
||||
},
|
||||
{
|
||||
"name": "Action?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area === 'action' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "48f78459-b87c-527d-a569-b3e753e4c0d4"
|
||||
},
|
||||
{
|
||||
"name": "Refuse action",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ question: $json.question, area: 'action', router: $json.router, refused: true, reason: 'read_only', answer: '운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요.' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
560
|
||||
],
|
||||
"id": "6ede178d-153c-53d7-a773-9b2bda2af049"
|
||||
},
|
||||
{
|
||||
"name": "Keyword hit?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area !== 'unknown' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "bd2aea65-6cee-5229-b19a-5ec357ba6bc1"
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.question }}",
|
||||
"options": {
|
||||
"systemMessage": "Classify an IT helpdesk operator's question (Korean) into one area of the operations data. Areas: runs (helpdesk runs: errors, failures, slowness, response time / 실행 · 오류 · 느림), alerts (alert messages / 알림), dlq (intake items that failed and wait for replay / 접수 못 한 것 · 남은 것 · 재처리 대기), quality (FAQ data batches, quality scores, quarantine / 데이터 품질 · FAQ 배치 · 격리), cost (LLM spending, money, budget, tokens / 비용 · 돈 · 지출 · 얼마 나갔나), release (versions, canary, rollout to some users / 버전 · 새 판 · 일부에게 먼저 내보냄), overview (is everything ok, general status), none (anything not about operating the helpdesk). Reply with only the area word.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput",
|
||||
"position": [
|
||||
880,
|
||||
140
|
||||
],
|
||||
"id": "6747acdd-c6b6-54a5-9707-f995bcf54be6"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
-60
|
||||
],
|
||||
"id": "9e1b1e99-9563-50eb-ae41-edba7312bd00"
|
||||
},
|
||||
{
|
||||
"name": "Read class",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Route').first().json;\nconst word = String($json.output ?? '').trim().toLowerCase().replace(/[^a-z]/g, '');\nconst known = ['runs', 'alerts', 'dlq', 'quality', 'cost', 'release', 'overview'];\nreturn [{ json: { ...r, area: known.includes(word) ? word : 'unknown', router: 'llm' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
140
|
||||
],
|
||||
"id": "a36bcee6-b885-5021-9b88-5321ed56c3b4"
|
||||
},
|
||||
{
|
||||
"name": "Routed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return [{ json: $input.first().json }];"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "db234186-80da-5094-9fdc-9b85b9fffd56"
|
||||
},
|
||||
{
|
||||
"name": "Known area?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area !== 'unknown' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "a904a046-a9d2-5f43-bc24-45c10bb803fb"
|
||||
},
|
||||
{
|
||||
"name": "Out of scope",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ question: $json.question, area: 'unknown', router: $json.router, refused: true, reason: 'out_of_scope', answer: '운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다.' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
560
|
||||
],
|
||||
"id": "047a7d3f-bc56-5a24-ae65-8b2143880a44"
|
||||
},
|
||||
{
|
||||
"name": "Get facts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://localhost:5678/webhook/my-ops-facts",
|
||||
"options": {
|
||||
"timeout": 30000
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $('Ask webhook').first().json.body?.from ?? '', to: $('Ask webhook').first().json.body?.to ?? '' }) }}"
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "adea6e67-7abd-55a9-8659-98b7e285cce3"
|
||||
},
|
||||
{
|
||||
"name": "Pick facts",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Routed').first().json;\nconst f = $('Get facts').first().json;\nconst part = r.area === 'overview' ? f : { period: f.period, [r.area]: f[r.area], ...(f.health ? { health: f.health[r.area] } : {}) };\nreturn [{ json: { ...r, facts: part } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
],
|
||||
"id": "7550791a-4b6c-57a0-82a8-1f97ab9dbe79"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $json.question }}\n사실(JSON): {{ JSON.stringify($json.facts) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the operations assistant of a company IT helpdesk team. Answer the operator's question in Korean, 2-3 sentences, using only the facts JSON you are given. Use only numbers that appear in the facts (a ratio like 0.0588 may be written as 5.88%). Do not guess causes that are not in the facts. Do not offer to run any action; you can only read.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
],
|
||||
"id": "83bb5d6e-92c7-5d43-bc11-78f9d4b23568"
|
||||
},
|
||||
{
|
||||
"name": "Answer model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
520
|
||||
],
|
||||
"id": "3e644835-aa33-5c70-b0aa-c18d300579b3"
|
||||
},
|
||||
{
|
||||
"name": "Check numbers",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Pick facts').first().json;\nconst answer = String($json.output ?? '').trim();\nconst allowed = new Set();\nconst add = (v) => { for (const d of [0, 1, 2, 3, 4, 6]) allowed.add(Number(v.toFixed(d))); };\nconst walk = (v) => {\n if (typeof v === 'number') { add(v); add(v * 100); }\n else if (typeof v === 'string') { for (const m of v.match(/\\d+(?:\\.\\d+)?/g) ?? []) add(Number(m)); }\n else if (v && typeof v === 'object') { for (const [k, x] of Object.entries(v)) { walk(k); walk(x); } }\n};\nwalk(r.facts);\nconst versions = JSON.stringify(r.facts).match(/\\d+\\.\\d+\\.\\d+/g) ?? [];\nlet text = answer;\nfor (const v of versions) text = text.split(v).join(' ');\nconst unknown = (text.match(/\\d+(?:\\.\\d+)?/g) ?? []).filter((m) => !allowed.has(Number(m)));\nreturn [{ json: { question: r.question, area: r.area, router: r.router, refused: false, reason: null, answer,\n numbers_ok: unknown.length === 0, unknown_numbers: [...new Set(unknown)] } }];"
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
300
|
||||
],
|
||||
"id": "51f5e16b-f31f-5606-8b33-7271c651764f"
|
||||
},
|
||||
{
|
||||
"name": "Send answer",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2640,
|
||||
300
|
||||
],
|
||||
"id": "a1f84cfd-5942-59f2-81c8-613559203d2a"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Ask webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Route",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Route": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Action?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Action?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Refuse action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Keyword hit?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Keyword hit?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Routed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read class",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read class": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Routed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Routed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Known area?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Known area?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Out of scope",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pick facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pick facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check numbers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check numbers": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
{
|
||||
"name": "79 Ops ask + audit (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Ask webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-ask",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "e0cc9a1d-38cf-580a-b52e-3f6d8bb1cab2",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "a2d5e198-0074-5f7f-9cd6-5203c98b0bb1"
|
||||
},
|
||||
{
|
||||
"name": "Route",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const q = String($('Ask webhook').first().json.body?.question ?? '').trim();\nconst lower = q.toLowerCase();\nconst words = {\n runs: ['오류', '에러', '실패율', '느려', '느린', '응답 시간', 'p95', '실행'],\n alerts: ['알림', '경보'],\n dlq: ['dlq', '재처리', '쌓인', 'parked', '접수 실패'],\n quality: ['품질', '배치', '격리'],\n cost: ['비용', '돈', '예산', '토큰', '요금'],\n release: ['릴리스', '버전', '카나리', '배포'],\n overview: ['괜찮', '상태', '요약', '전체', '브리핑'],\n};\nconst action = /(다시 돌려|재처리해|지워|삭제해|꺼 ?줘|켜 ?줘|배포해|되돌려|롤백해|바꿔 ?줘|올려 ?줘|내려 ?줘|비워 ?줘)/.test(q);\nif (action) return [{ json: { question: q, area: 'action', router: 'keyword' } }];\nlet area = 'unknown', best = 0;\nfor (const [k, list] of Object.entries(words)) {\n const n = list.filter((w) => lower.includes(w)).length;\n if (n > best) { best = n; area = k; }\n}\nreturn [{ json: { question: q, area, router: 'keyword' } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "a5744b67-3600-5a8a-8607-d6c0896313f1"
|
||||
},
|
||||
{
|
||||
"name": "Action?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area === 'action' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "48f78459-b87c-527d-a569-b3e753e4c0d4"
|
||||
},
|
||||
{
|
||||
"name": "Refuse action",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ question: $('Route').first().json.question, area: 'action', router: $('Route').first().json.router, refused: true, reason: 'read_only', answer: '운영 도우미는 읽기만 합니다. 재처리 · 삭제 · 배포 같은 작업은 런북에 따라 담당자가 직접 해 주세요.' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
560
|
||||
],
|
||||
"id": "6ede178d-153c-53d7-a773-9b2bda2af049"
|
||||
},
|
||||
{
|
||||
"name": "Keyword hit?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area !== 'unknown' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "bd2aea65-6cee-5229-b19a-5ec357ba6bc1"
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.question }}",
|
||||
"options": {
|
||||
"systemMessage": "Classify an IT helpdesk operator's question (Korean) into one area of the operations data. Areas: runs (helpdesk runs: errors, failures, slowness, response time / 실행 · 오류 · 느림), alerts (alert messages / 알림), dlq (intake items that failed and wait for replay / 접수 못 한 것 · 남은 것 · 재처리 대기), quality (FAQ data batches, quality scores, quarantine / 데이터 품질 · FAQ 배치 · 격리), cost (LLM spending, money, budget, tokens / 비용 · 돈 · 지출 · 얼마 나갔나), release (versions, canary, rollout to some users / 버전 · 새 판 · 일부에게 먼저 내보냄), overview (is everything ok, general status), none (anything not about operating the helpdesk). Reply with only the area word.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput",
|
||||
"position": [
|
||||
880,
|
||||
140
|
||||
],
|
||||
"id": "6747acdd-c6b6-54a5-9707-f995bcf54be6"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
-60
|
||||
],
|
||||
"id": "9e1b1e99-9563-50eb-ae41-edba7312bd00"
|
||||
},
|
||||
{
|
||||
"name": "Read class",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Route').first().json;\nconst word = String($json.output ?? '').trim().toLowerCase().replace(/[^a-z]/g, '');\nconst known = ['runs', 'alerts', 'dlq', 'quality', 'cost', 'release', 'overview'];\nreturn [{ json: { ...r, area: known.includes(word) ? word : 'unknown', router: 'llm' } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
140
|
||||
],
|
||||
"id": "a36bcee6-b885-5021-9b88-5321ed56c3b4"
|
||||
},
|
||||
{
|
||||
"name": "Routed",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return [{ json: $input.first().json }];"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "db234186-80da-5094-9fdc-9b85b9fffd56"
|
||||
},
|
||||
{
|
||||
"name": "Known area?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.area !== 'unknown' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "a904a046-a9d2-5f43-bc24-45c10bb803fb"
|
||||
},
|
||||
{
|
||||
"name": "Out of scope",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ question: $('Routed').first().json.question, area: 'unknown', router: $('Routed').first().json.router, refused: true, reason: 'out_of_scope', answer: '운영 데이터(실행 · 알림 · DLQ · 품질 · 비용 · 릴리스)로 답할 수 있는 질문만 받습니다.' }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
560
|
||||
],
|
||||
"id": "047a7d3f-bc56-5a24-ae65-8b2143880a44"
|
||||
},
|
||||
{
|
||||
"name": "Get facts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://localhost:5678/webhook/my-ops-facts",
|
||||
"options": {
|
||||
"timeout": 30000
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ from: $('Ask webhook').first().json.body?.from ?? '', to: $('Ask webhook').first().json.body?.to ?? '' }) }}"
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "adea6e67-7abd-55a9-8659-98b7e285cce3"
|
||||
},
|
||||
{
|
||||
"name": "Pick facts",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Routed').first().json;\nconst f = $('Get facts').first().json;\nconst part = r.area === 'overview' ? f : { period: f.period, [r.area]: f[r.area], ...(f.health ? { health: f.health[r.area] } : {}) };\nreturn [{ json: { ...r, facts: part } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
],
|
||||
"id": "7550791a-4b6c-57a0-82a8-1f97ab9dbe79"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $json.question }}\n사실(JSON): {{ JSON.stringify($json.facts) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the operations assistant of a company IT helpdesk team. Answer the operator's question in Korean, 2-3 sentences, using only the facts JSON you are given. Use only numbers that appear in the facts (a ratio like 0.0588 may be written as 5.88%). Do not guess causes that are not in the facts. Do not offer to run any action; you can only read.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
],
|
||||
"id": "83bb5d6e-92c7-5d43-bc11-78f9d4b23568"
|
||||
},
|
||||
{
|
||||
"name": "Answer model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
520
|
||||
],
|
||||
"id": "3e644835-aa33-5c70-b0aa-c18d300579b3"
|
||||
},
|
||||
{
|
||||
"name": "Check numbers",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const r = $('Pick facts').first().json;\nconst answer = String($json.output ?? '').trim();\nconst allowed = new Set();\nconst add = (v) => { for (const d of [0, 1, 2, 3, 4, 6]) allowed.add(Number(v.toFixed(d))); };\nconst walk = (v) => {\n if (typeof v === 'number') { add(v); add(v * 100); }\n else if (typeof v === 'string') { for (const m of v.match(/\\d+(?:\\.\\d+)?/g) ?? []) add(Number(m)); }\n else if (v && typeof v === 'object') { for (const [k, x] of Object.entries(v)) { walk(k); walk(x); } }\n};\nwalk(r.facts);\nconst versions = JSON.stringify(r.facts).match(/\\d+\\.\\d+\\.\\d+/g) ?? [];\nlet text = answer;\nfor (const v of versions) text = text.split(v).join(' ');\nconst unknown = (text.match(/\\d+(?:\\.\\d+)?/g) ?? []).filter((m) => !allowed.has(Number(m)));\nreturn [{ json: { question: r.question, area: r.area, router: r.router, refused: false, reason: null, answer,\n numbers_ok: unknown.length === 0, unknown_numbers: [...new Set(unknown)] } }];"
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
300
|
||||
],
|
||||
"id": "51f5e16b-f31f-5606-8b33-7271c651764f"
|
||||
},
|
||||
{
|
||||
"name": "Send answer",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $('Check numbers').first().json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2860,
|
||||
300
|
||||
],
|
||||
"id": "a1f84cfd-5942-59f2-81c8-613559203d2a"
|
||||
},
|
||||
{
|
||||
"name": "Audit action",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
660,
|
||||
560
|
||||
],
|
||||
"id": "d3d7581f-619f-5c38-9c1a-3518405c9d98",
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/audit",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ kind: 'ops-ask', question: $('Route').first().json.question, area: $('Route').first().json.area, router: $('Route').first().json.router, refused: true, reason: 'read_only', numbers_ok: null }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Audit scope",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1760,
|
||||
560
|
||||
],
|
||||
"id": "78e261a3-2b14-5108-a31c-eda31046bddb",
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/audit",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ kind: 'ops-ask', question: $('Routed').first().json.question, area: $('Routed').first().json.area, router: $('Routed').first().json.router, refused: true, reason: 'out_of_scope', numbers_ok: null }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Audit answer",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
2640,
|
||||
300
|
||||
],
|
||||
"id": "b20126ed-a5d2-5e91-91cd-1e9cacd0279f",
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/audit",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ kind: 'ops-ask', question: $('Check numbers').first().json.question, area: $('Check numbers').first().json.area, router: $('Check numbers').first().json.router, refused: false, reason: null, numbers_ok: $('Check numbers').first().json.numbers_ok }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Ask webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Route",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Route": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Action?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Action?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Audit action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Keyword hit?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Keyword hit?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Routed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read class",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read class": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Routed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Routed": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Known area?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Known area?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Audit scope",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pick facts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pick facts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check numbers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check numbers": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Audit answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Audit action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Refuse action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Audit scope": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Out of scope",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Audit answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"name": "79 Ops assistant eval (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Eval webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-eval",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "b9e92341-ff71-5352-9909-618024a18357",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "79dec3c9-bffe-5565-8228-bb186d76ab2d"
|
||||
},
|
||||
{
|
||||
"name": "Has cases?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.cases) && $json.body.cases.length > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "f8f0ded2-67fd-5079-be71-51cf8352a46e"
|
||||
},
|
||||
{
|
||||
"name": "No cases",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"cases must be a non-empty list\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
520
|
||||
],
|
||||
"id": "4d2e4cbd-d282-5859-9b7c-09d10a1f52d2"
|
||||
},
|
||||
{
|
||||
"name": "Split cases",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $('Eval webhook').first().json.body.cases.map((c) => ({ json: c }));"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "c7d458a8-2b05-5b5d-8622-a449fc96947f"
|
||||
},
|
||||
{
|
||||
"name": "Ask",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://localhost:5678/webhook/my-ops-ask",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
},
|
||||
"timeout": 60000
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ question: $json.question }) }}"
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "637c2a81-6496-51a3-96ea-e1e7746a3e79"
|
||||
},
|
||||
{
|
||||
"name": "Score",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const cases = $('Eval webhook').first().json.body.cases;\nconst answers = $input.all().map((i) => i.json);\nconst results = cases.map((c, i) => {\n const a = answers[i] ?? {};\n const areaOk = a.area === c.area;\n const refusedOk = Boolean(a.refused) === Boolean(c.refused);\n const numberOk = !c.numbers || c.numbers.some((n) => String(a.answer ?? '').includes(n));\n const grounded = a.refused ? true : a.numbers_ok === true;\n return { question: c.question, area: a.area ?? null, ok: areaOk && refusedOk && numberOk && grounded, area_ok: areaOk, refused_ok: refusedOk, number_ok: numberOk, grounded, answer: a.answer ?? null };\n});\nconst n = results.length, cnt = (k) => results.filter((r) => r[k]).length;\nreturn [{ json: { total: n, passed: cnt('ok'), score: n ? Math.round(cnt('ok') / n * 10000) / 10000 : 0,\n area_ok: cnt('area_ok'), refused_ok: cnt('refused_ok'), number_ok: cnt('number_ok'), grounded: cnt('grounded'),\n failed: results.filter((r) => !r.ok) } }];"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "97e3d80b-97c6-534c-b854-247fbb2d3aa1"
|
||||
},
|
||||
{
|
||||
"name": "Send score",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "fe6103e3-3723-502b-ba34-f95ea2aacfd0"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Eval webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has cases?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has cases?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split cases",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "No cases",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split cases": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Ask",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Ask": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Score",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Score": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send score",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"name": "79 Ops assistant eval + gate (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Eval webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-eval",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "b9e92341-ff71-5352-9909-618024a18357",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "79dec3c9-bffe-5565-8228-bb186d76ab2d"
|
||||
},
|
||||
{
|
||||
"name": "Has cases?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.cases) && $json.body.cases.length > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "f8f0ded2-67fd-5079-be71-51cf8352a46e"
|
||||
},
|
||||
{
|
||||
"name": "No cases",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"cases must be a non-empty list\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
520
|
||||
],
|
||||
"id": "4d2e4cbd-d282-5859-9b7c-09d10a1f52d2"
|
||||
},
|
||||
{
|
||||
"name": "Split cases",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $('Eval webhook').first().json.body.cases.map((c) => ({ json: c }));"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "c7d458a8-2b05-5b5d-8622-a449fc96947f"
|
||||
},
|
||||
{
|
||||
"name": "Ask",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://localhost:5678/webhook/my-ops-ask",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
},
|
||||
"timeout": 60000
|
||||
},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ question: $json.question }) }}"
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "637c2a81-6496-51a3-96ea-e1e7746a3e79"
|
||||
},
|
||||
{
|
||||
"name": "Score",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const cases = $('Eval webhook').first().json.body.cases;\nconst answers = $input.all().map((i) => i.json);\nconst results = cases.map((c, i) => {\n const a = answers[i] ?? {};\n const areaOk = a.area === c.area;\n const refusedOk = Boolean(a.refused) === Boolean(c.refused);\n const numberOk = !c.numbers || c.numbers.some((n) => String(a.answer ?? '').includes(n));\n const grounded = a.refused ? true : a.numbers_ok === true;\n return { question: c.question, area: a.area ?? null, ok: areaOk && refusedOk && numberOk && grounded, area_ok: areaOk, refused_ok: refusedOk, number_ok: numberOk, grounded, answer: a.answer ?? null };\n});\nconst n = results.length, cnt = (k) => results.filter((r) => r[k]).length;\nconst min = Number($('Eval webhook').first().json.body.min_score ?? 0.9);\nconst sc = n ? Math.round(cnt('ok') / n * 10000) / 10000 : 0;\nreturn [{ json: { total: n, passed: cnt('ok'), score: n ? Math.round(cnt('ok') / n * 10000) / 10000 : 0,\n area_ok: cnt('area_ok'), refused_ok: cnt('refused_ok'), number_ok: cnt('number_ok'), grounded: cnt('grounded'),\n failed: results.filter((r) => !r.ok), min_score: min, gate: sc >= min ? 'pass' : 'fail' } }];"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "97e3d80b-97c6-534c-b854-247fbb2d3aa1"
|
||||
},
|
||||
{
|
||||
"name": "Send score",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
],
|
||||
"id": "fe6103e3-3723-502b-ba34-f95ea2aacfd0"
|
||||
},
|
||||
{
|
||||
"name": "Below?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "ef1a28a4-8753-597e-ae45-334233921758",
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.gate === 'fail' }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Alert eval",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1320,
|
||||
160
|
||||
],
|
||||
"id": "acd6f4e0-0aa0-5f71-9a62-defe5e28d3a2",
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ workflow: 'ops-eval', node: 'Below?', severity: 'medium', reason: 'eval_below', score: $('Score').first().json.score, min_score: $('Score').first().json.min_score, message: 'ops assistant eval ' + $('Score').first().json.score + ' < ' + $('Score').first().json.min_score }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Result again",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "679d16cb-7d8c-5d14-9b02-b0132a5a38e1",
|
||||
"parameters": {
|
||||
"jsCode": "return [{ json: $('Score').first().json }];"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Eval webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has cases?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has cases?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split cases",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "No cases",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split cases": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Ask",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Ask": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Score",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Score": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Below?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Below?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Alert eval",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Result again",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Alert eval": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Result again",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Result again": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send score",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user