Files

761 lines
22 KiB
Plaintext
Raw Permalink Normal View History

$ uv run tools/flow.py push answers/78-lab03-challenge2-check-log.json answers/78-lab03-challenge2-report.json
! unpublished '78 Schema check (reference)' (w6b17d7c76908a6a): it used the same webhook path
! unpublished '78 Quality report (answer)' (wa6b87a0a6957e93): it used the same webhook path
✔ pushed '78 Schema check + quality log (answer)' (id wf4c2f04db13849f)
POST http://localhost:5678/webhook/my-check
✔ pushed '78 Quality report + by dataset (answer)' (id we40b9a92abbf7fd)
POST http://localhost:5678/webhook/my-quality-report
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 5}
→ request POST http://localhost:8800/faq-reset
{}
← response HTTP 200
{
"cleared": 5
}
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/quarantine-clear
{}
← response HTTP 200
{
"cleared": 1
}
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/quality-log-clear
{}
← response HTTP 200
{
"cleared": 1
}
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/alerts-clear
{}
← response HTTP 200
{
"cleared": 1
}
✔ before: POST /webhook/my-check · HTTP 200 · 0.9s · {"schema": "employee", "batch": "HR-1", "total": 9, "valid": 4, "invalid": 5, "rows": [{"row": 1, "key": "sehoon.oh@exam
→ request POST /webhook/my-check
{
"schema": "employee",
"batch": "HR-1",
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
}
← response HTTP 200
{
"schema": "employee",
"batch": "HR-1",
"total": 9,
"valid": 4,
"invalid": 5,
"rows": [
{
"row": 1,
"key": "sehoon.oh@example.com",
"ok": true,
"errors": []
},
{
"row": 2,
"key": "haneul.yoon@example.com",
"ok": true,
"errors": []
},
{
"row": 3,
"key": "minsu.kang@example",
"ok": false,
"errors": [
"bad_format:email"
]
},
{
"row": 4,
"key": "jia.seo@example.com",
"ok": false,
"errors": [
"not_allowed:dept"
]
},
{
"row": 5,
"key": "doyun.lim@example.com",
"ok": false,
"errors": [
"bad_format:joined"
]
},
{
"row": 6,
"key": "noname@example.com",
"ok": false,
"errors": [
"missing:name"
]
},
{
"row": 7,
"key": "sua.bae@example.com",
"ok": true,
"errors": []
},
{
"row": 8,
"key": "sua.bae@example.com",
"ok": false,
"errors": [
"duplicate_in_batch"
]
},
{
"row": 9,
"key": "daeun.jung@example.com",
"ok": true,
"errors": []
}
],
"codes": {
"bad_format:email": 1,
"not_allowed:dept": 1,
"bad_format:joined": 1,
"missing:name": 1,
"duplicate_in_batch": 1
}
}
✔ before: POST /webhook/my-check · HTTP 200 · 0.3s · {"schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [{"row": 1, "key": "T-2001", "ok": t
→ request POST /webhook/my-check
{
"schema": "ticket",
"batch": "TK-1",
"items": [
{
"ticket_id": "T-2001",
"title": "모니터가 켜지지 않음",
"requester": "박서연",
"priority": "normal",
"opened": "2026-10-28",
"contact": "010-2345-6789"
},
{
"ticket_id": "T-12",
"title": "메일 첨부가 안 열림",
"requester": "이준호",
"priority": "low",
"opened": "2026-10-28"
},
{
"ticket_id": "T-2003",
"title": "VPN이 계속 끊김",
"requester": "최유진",
"priority": "urgent",
"opened": "2026-10-29"
},
{
"ticket_id": "T-2004",
"title": "키보드 교체 요청",
"requester": "한지훈",
"priority": "low",
"opened": "2026-02-30"
},
{
"ticket_id": "T-2005",
"title": "프린터",
"requester": "김민지",
"priority": "normal",
"opened": "2026-10-29"
},
{
"ticket_id": "T-2006",
"title": "공유 드라이브 권한",
"requester": "박서연",
"priority": "normal",
"opened": "2026-10-30",
"contact": "010-12-3456"
},
{
"ticket_id": "T-2001",
"title": "모니터가 또 꺼짐",
"requester": "박서연",
"priority": "high",
"opened": "2026-10-30"
},
{
"ticket_id": "T-2008",
"title": "화상회의 카메라 대여",
"requester": "",
"priority": "low",
"opened": "2026-10-30"
},
{
"ticket_id": "T-2009",
"title": "새 직원 계정 만들기",
"requester": "이준호",
"priority": "high",
"opened": "2026-10-30"
}
]
}
← response HTTP 200
{
"schema": "ticket",
"batch": "TK-1",
"total": 9,
"valid": 2,
"invalid": 7,
"rows": [
{
"row": 1,
"key": "T-2001",
"ok": true,
"errors": []
},
{
"row": 2,
"key": "T-12",
"ok": false,
"errors": [
"bad_format:ticket_id"
]
},
{
"row": 3,
"key": "T-2003",
"ok": false,
"errors": [
"not_allowed:priority"
]
},
{
"row": 4,
"key": "T-2004",
"ok": false,
"errors": [
"bad_format:opened"
]
},
{
"row": 5,
"key": "T-2005",
"ok": false,
"errors": [
"too_short:title"
]
},
{
"row": 6,
"key": "T-2006",
"ok": false,
"errors": [
"bad_format:contact"
]
},
{
"row": 7,
"key": "T-2001",
"ok": false,
"errors": [
"duplicate_in_batch"
]
},
{
"row": 8,
"key": "T-2008",
"ok": false,
"errors": [
"missing:requester"
]
},
{
"row": 9,
"key": "T-2009",
"ok": true,
"errors": []
}
],
"codes": {
"bad_format:ticket_id": 1,
"not_allowed:priority": 1,
"bad_format:opened": 1,
"too_short:title": 1,
"bad_format:contact": 1,
"duplicate_in_batch": 1,
"missing:requester": 1
}
}
✔ before: POST /webhook/my-faq-import · HTTP 200 · 5.0s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
→ request POST /webhook/my-faq-import
{
"batch": "B-3",
"items": [
{
"id": "FAQ-301",
"title": "택배 수령",
"keywords": [
"택배"
],
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
},
{
"id": "FAQ-302",
"title": "주차 등록",
"keywords": [
"주차"
],
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
},
{
"id": "FAQ-303",
"title": "명함 신청",
"keywords": [
"명함"
],
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
},
{
"id": "FAQ-304",
"title": "사내 식당 메뉴",
"keywords": [
"식당",
"메뉴"
],
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
},
{
"id": "FAQ-305",
"title": "교육 신청",
"keywords": [
"교육"
],
"answer": "짧"
},
{
"id": "FAQ-306",
"title": "건강검진",
"keywords": [
"건강검진"
],
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
}
]
}
← response HTTP 200
{
"batch": "B-3",
"total": 6,
"imported": 5,
"quarantined": 1,
"rejected": false,
"score": 0.8333,
"max_invalid_ratio": 0.3,
"codes": {
"answer_too_short": 1
},
"imported_ids": [
"FAQ-301",
"FAQ-302",
"FAQ-303",
"FAQ-304",
"FAQ-306"
],
"quarantined_ids": [
"FAQ-305"
],
"llm_calls": 5
}
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
✔ 검사도 품질 기록 (직원 · 티켓 · FAQ) · 0.0s
→ request POST http://localhost:8800/quality-log/list
{}
← response HTTP 200
{
"count": 3,
"items": [
{
"batch": "HR-1",
"dataset": "employee",
"total": 9,
"valid": 4,
"quarantined": 5,
"rejected": false,
"score": 0.4444,
"codes": {
"bad_format:email": 1,
"not_allowed:dept": 1,
"bad_format:joined": 1,
"missing:name": 1,
"duplicate_in_batch": 1
},
"at": "2026-11-01T18:11:30"
},
{
"batch": "TK-1",
"dataset": "ticket",
"total": 9,
"valid": 2,
"quarantined": 7,
"rejected": false,
"score": 0.2222,
"codes": {
"bad_format:ticket_id": 1,
"not_allowed:priority": 1,
"bad_format:opened": 1,
"too_short:title": 1,
"bad_format:contact": 1,
"duplicate_in_batch": 1,
"missing:requester": 1
},
"at": "2026-11-01T18:11:30"
},
{
"batch": "B-3",
"dataset": "faq",
"total": 6,
"valid": 5,
"quarantined": 1,
"rejected": false,
"score": 0.8333,
"codes": {
"answer_too_short": 1
},
"at": "2026-11-01T18:11:35"
}
]
}
✔ 데이터셋마다 나눠 보기 · 0.1s
→ request POST /webhook/my-quality-report
{}
← response HTTP 200
{
"count": 3,
"batches": [
{
"batch": "HR-1",
"score": 0.4444,
"rejected": false,
"total": 9
},
{
"batch": "TK-1",
"score": 0.2222,
"rejected": false,
"total": 9
},
{
"batch": "B-3",
"score": 0.8333,
"rejected": false,
"total": 6
}
],
"avg_score": 0.5,
"worst_batch": "TK-1",
"rejected_batches": 0,
"top_codes": [
{
"code": "duplicate_in_batch",
"count": 2
},
{
"code": "answer_too_short",
"count": 1
},
{
"code": "bad_format:contact",
"count": 1
},
{
"code": "bad_format:email",
"count": 1
},
{
"code": "bad_format:joined",
"count": 1
},
{
"code": "bad_format:opened",
"count": 1
},
{
"code": "bad_format:ticket_id",
"count": 1
},
{
"code": "missing:name",
"count": 1
},
{
"code": "missing:requester",
"count": 1
},
{
"code": "not_allowed:dept",
"count": 1
},
{
"code": "not_allowed:priority",
"count": 1
},
{
"code": "too_short:title",
"count": 1
}
],
"trend": "좋아짐",
"by_dataset": {
"employee": {
"count": 1,
"avg_score": 0.4444,
"worst_batch": "HR-1",
"top_code": "bad_format:email"
},
"ticket": {
"count": 1,
"avg_score": 0.2222,
"worst_batch": "TK-1",
"top_code": "bad_format:contact"
},
"faq": {
"count": 1,
"avg_score": 0.8333,
"worst_batch": "B-3",
"top_code": "answer_too_short"
}
},
"worst_dataset": "ticket"
}
✔ 가장 나쁜 데이터셋 · 그 이유 · 0.0s
→ request POST /webhook/my-quality-report
{}
← response HTTP 200
{
"count": 3,
"batches": [
{
"batch": "HR-1",
"score": 0.4444,
"rejected": false,
"total": 9
},
{
"batch": "TK-1",
"score": 0.2222,
"rejected": false,
"total": 9
},
{
"batch": "B-3",
"score": 0.8333,
"rejected": false,
"total": 6
}
],
"avg_score": 0.5,
"worst_batch": "TK-1",
"rejected_batches": 0,
"top_codes": [
{
"code": "duplicate_in_batch",
"count": 2
},
{
"code": "answer_too_short",
"count": 1
},
{
"code": "bad_format:contact",
"count": 1
},
{
"code": "bad_format:email",
"count": 1
},
{
"code": "bad_format:joined",
"count": 1
},
{
"code": "bad_format:opened",
"count": 1
},
{
"code": "bad_format:ticket_id",
"count": 1
},
{
"code": "missing:name",
"count": 1
},
{
"code": "missing:requester",
"count": 1
},
{
"code": "not_allowed:dept",
"count": 1
},
{
"code": "not_allowed:priority",
"count": 1
},
{
"code": "too_short:title",
"count": 1
}
],
"trend": "좋아짐",
"by_dataset": {
"employee": {
"count": 1,
"avg_score": 0.4444,
"worst_batch": "HR-1",
"top_code": "bad_format:email"
},
"ticket": {
"count": 1,
"avg_score": 0.2222,
"worst_batch": "TK-1",
"top_code": "bad_format:contact"
},
"faq": {
"count": 1,
"avg_score": 0.8333,
"worst_batch": "B-3",
"top_code": "answer_too_short"
}
},
"worst_dataset": "ticket"
}
✔ 한 데이터셋만 (앞의 칸은 그대로) · 0.1s
→ request POST /webhook/my-quality-report
{
"dataset": "ticket"
}
← response HTTP 200
{
"count": 1,
"batches": [
{
"batch": "TK-1",
"score": 0.2222,
"rejected": false,
"total": 9
}
],
"avg_score": 0.2222,
"worst_batch": "TK-1",
"rejected_batches": 0,
"top_codes": [
{
"code": "bad_format:contact",
"count": 1
},
{
"code": "bad_format:opened",
"count": 1
},
{
"code": "bad_format:ticket_id",
"count": 1
},
{
"code": "duplicate_in_batch",
"count": 1
},
{
"code": "missing:requester",
"count": 1
},
{
"code": "not_allowed:priority",
"count": 1
},
{
"code": "too_short:title",
"count": 1
}
],
"trend": "그대로",
"by_dataset": {
"employee": {
"count": 1,
"avg_score": 0.4444,
"worst_batch": "HR-1",
"top_code": "bad_format:email"
},
"ticket": {
"count": 1,
"avg_score": 0.2222,
"worst_batch": "TK-1",
"top_code": "bad_format:contact"
},
"faq": {
"count": 1,
"avg_score": 0.8333,
"worst_batch": "B-3",
"top_code": "answer_too_short"
}
},
"worst_dataset": "ticket"
}
Passed 4/4
report: runs/lab03-challenge2-20261101-181135.json
$ uv run tools/flow.py test cases/lab03.json
POST http://localhost:5678/webhook/my-check · 4 case(s) × 1
✔ 직원 CSV · 9줄 중 4줄 통과 · 0.1s
✔ 직원 CSV · 줄마다 이유 · 0.1s
✔ 같은 워크플로우로 FAQ 스키마 · 0.1s
✔ 모르는 스키마 → 400 · 0.0s
Passed 4/4
report: runs/lab03-20261101-181136.json
$ uv run tools/flow.py test cases/lab03-challenge.json
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 5}
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 6}
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
! before: POST /webhook/my-faq-import · HTTP 422 · 0.1s (allow_fail: rejected on purpose) · {"error": "batch rejected", "batch": "B-1", "invalid_ratio": 0.7692, "max_invalid_ratio": 0.3, "codes": {"title_missing"
✔ before: POST /webhook/my-faq-import · HTTP 200 · 8.8s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
✔ before: POST /webhook/my-faq-import · HTTP 200 · 4.6s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
✔ before: POST /webhook/my-faq-import · HTTP 200 · 1.8s · {"batch": "B-2-fix", "total": 2, "imported": 2, "quarantined": 0, "rejected": false, "score": 1, "max_invalid_ratio": 0.
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
✔ 배치 네 개 · 평균 · 가장 나쁜 배치 · 0.0s
✔ 가장 많은 이유 · 흐름 · 0.1s
✔ 배치마다 점수 순서 · 0.0s
✔ 다른 데이터셋으로 거르기 · 0.1s
Passed 4/4
report: runs/lab03-challenge-20261101-181151.json
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 15}
$ uv run tools/flow.py quality
FAQ: 6 (nothing added)
quarantine: 1
B-3 FAQ-305 answer_too_short
quality log: 4
B-1 faq total 13 valid 3 score 0 rejected True
B-2 faq total 10 valid 8 score 0.8 rejected False
B-3 faq total 6 valid 5 score 0.8333 rejected False
B-2-fix faq total 2 valid 2 score 1 rejected False
alerts: 3
[high] faq-import · batch_rejected · B-1 rejected
[high] faq-import · low_quality · B-2 quality 0.8
[high] faq-import · low_quality · B-3 quality 0.8333
$ uv run tools/flow.py ops --reset
✔ live run logs, added FAQ, quarantine, quality log, created tickets, DLQ and alerts emptied, fault switch all ok
$ uv run tools/flow.py doctor
! n8n install folder /Users/mcchae/day65-n8n
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
✔ n8n container day65-n8n-n8n-1
✔ n8n http://localhost:5678
✔ data API http://localhost:8800
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
✔ run logs /logs (666 seeded runs) · FAQ additions /faq-extra (day 77 data API)
✔ schemas faq · employee · ticket, quarantine, quality log (day 78 data API)
✔ n8n → http://data-api:8800
✔ credential 'OpenAI (.env)' (openAiCourse0001)
✔ node catalog (for check)
✔ CLI coding agent: codex, claude, agy
ready