75일차 답 추가: 실습 정답 문서 · VIBE-A · 정답 워크플로우와 25단계 실행 기록 · tcost

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcchaeandClaude Opus 5.5 committed 2026-10-29 09:57:32 +09:00
1 parent 01dc892303
commit ebff58c72e
79 files changed
+18753

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
$ uv run tools/flow.py budget --reset
✔ budget back to data/budget.json
budget $1.0 / used $0.000000 · remaining $1.000000 · over False · per user 20/min · question 500 chars
$ uv run tools/flow.py usage --clear
✔ usage cleared: {"cleared": 0}
$ uv run tools/flow.py fault all ok
✔ fault switch: {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
$ uv run tools/flow.py push reference/baseline-cost.json
✔ pushed '75 IT helpdesk baseline (provided)' (id wdb6a99a8f13c6a1)
POST http://localhost:5678/webhook/base-cost
$ 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
✔ prices /prices · budget /budget (day 75 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,70 @@
$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
NAME CPU % MEM USAGE / LIMIT
day67-data-data-api-1 0.02% 20.3MiB / 7.818GiB
day65-n8n-runners-1 0.21% 100.5MiB / 7.818GiB
day65-n8n-n8n-1 0.27% 642.6MiB / 7.818GiB
$ uv run tools/flow.py call base-cost requests/faq-vpn.json
HTTP 200 · 4.3s
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66
}
}
}
}
$ uv run tools/flow.py call base-cost requests/ticket-own.json
HTTP 200 · 3.3s
{
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 연락 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 97,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 42
}
}
}
}
$ curl -s localhost:8800/prices
{"currency": "USD", "unit": "per 1M tokens", "note": "OpenAI 모델 문서의 공개 단가 (2026-10-28 확인). 수업 전에 가격표를 확인해 바꾸세요.", "models": {"gpt-4.1-mini": {"input": 0.4, "output": 1.6}, "gpt-5.4-nano": {"input": 0.2, "output": 1.25}}}
+114
View File
@@ -0,0 +1,114 @@
{
"cases": "lab01.json",
"webhook": "my-cost",
"passed": 3,
"total": 3,
"results": [
{
"case": "VPN · 비용과 단계별 사용량",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 5.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
},
{
"case": "접수 현황 · 비용",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.3,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 98,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 43,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000946
}
},
{
"case": "빈 질문은 LLM 없이",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 400,
"request": {
"question": "",
"user": "이준호"
},
"response": {
"error": "question is required"
}
}
]
}
+97
View File
@@ -0,0 +1,97 @@
$ uv run tools/flow.py push reference/lab01-example2-cost.json
! unpublished '75 IT helpdesk + user limit (reference)' (w885dc4a9b362094): it used the same webhook path
✔ pushed '75 IT helpdesk + cost (reference)' (id w7aad5ed3acdb113)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab01.json -v
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ VPN · 비용과 단계별 사용량 · 5.1s · 자동 답변 · 3 calls · $0.001040
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
✔ 접수 현황 · 비용 · 3.3s · 자동 답변 · 3 calls · $0.000946
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 98,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 43,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000946
}
✔ 빈 질문은 LLM 없이 · 0.0s
→ request POST /webhook/my-cost
{
"question": "",
"user": "이준호"
}
← response HTTP 400
{
"error": "question is required"
}
Passed 3/3
report: runs/lab01-20261028-210102.json
+324
View File
@@ -0,0 +1,324 @@
{
"cases": "lab01-mix.json",
"webhook": "my-cost",
"passed": 7,
"total": 7,
"results": [
{
"case": "FAQ · VPN",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
},
{
"case": "FAQ · 프린터",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.9,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1967,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 54,
"cost_usd": 0.000798
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 48,
"cost_usd": 0.0001232
}
}
},
"cost_usd": 0.0009596
}
},
{
"case": "FAQ · 비밀번호",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.5,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1978,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 46,
"cost_usd": 0.0007892
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 108,
"completion_tokens": 49,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.0009528
}
},
{
"case": "FAQ · 메일",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.4,
"status": 200,
"request": {
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
},
"response": {
"route": "자동 답변",
"answer": "메일함 용량이 가득 찼을 경우, 오래된 메일이나 큰 첨부 파일을 삭제하거나 보관함으로 이동하여 용량을 확보하시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주십시오.",
"sources": [
"FAQ-005"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1969,
"completion_tokens": 111,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 74,
"completion_tokens": 6,
"cost_usd": 3.92e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1780,
"completion_tokens": 52,
"cost_usd": 0.0007952
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 115,
"completion_tokens": 53,
"cost_usd": 0.0001308
}
}
},
"cost_usd": 0.0009652
}
},
{
"case": "접수 · T-1002",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 98,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 43,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000946
}
},
{
"case": "접수 · T-1001",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "T-1001 어떻게 됐나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체되었습니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1944,
"completion_tokens": 70,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 29,
"cost_usd": 0.0007588
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 91,
"completion_tokens": 35,
"cost_usd": 9.24e-05
}
}
},
"cost_usd": 0.0008896
}
},
{
"case": "규칙에 없는 질문",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.6,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "회의실 화면 공유 문제는 IT 문의로 연락 주시면 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1883,
"completion_tokens": 48,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1719,
"completion_tokens": 24,
"cost_usd": 0.000726
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 86,
"completion_tokens": 18,
"cost_usd": 6.32e-05
}
}
},
"cost_usd": 0.00083
}
}
]
}
+11
View File
@@ -0,0 +1,11 @@
$ uv run tools/flow.py test cases/lab01-mix.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ FAQ · VPN · 3.2s · 자동 답변 · 3 calls · $0.001040
✔ FAQ · 프린터 · 3.9s · 자동 답변 · 3 calls · $0.000960
✔ FAQ · 비밀번호 · 3.5s · 자동 답변 · 3 calls · $0.000953
✔ FAQ · 메일 · 3.4s · 자동 답변 · 3 calls · $0.000965
✔ 접수 · T-1002 · 3.0s · 자동 답변 · 3 calls · $0.000946
✔ 접수 · T-1001 · 3.2s · 자동 답변 · 3 calls · $0.000890
✔ 규칙에 없는 질문 · 2.6s · 자동 답변 · 3 calls · $0.000830
Passed 7/7
report: runs/lab01-mix-20261028-210125.json
@@ -0,0 +1,169 @@
{
"cases": "lab01-challenge.json",
"webhook": "my-cost",
"passed": 3,
"total": 4,
"results": [
{
"case": "기록 1 · VPN",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.5,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
},
{
"case": "기록 2 · 접수",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 98,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 43,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000946
}
},
{
"case": "기록 3 · 다른 사람",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.4,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548
}
},
{
"case": "사용량 요약 · 요청 3건",
"webhook": "http://localhost:8800/usage/summary",
"run": 1,
"passed": false,
"problems": [
"requests = 0 (expected 3)",
"cost_usd = 0.0 (expected 1e-06..0.05)",
"by_user has none of ['이준호', '박서연', '한지훈']",
"by_model has none of ['gpt-4.1-mini']"
],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 0,
"calls": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"cost_usd": 0.0,
"by_model": {},
"by_user": {},
"by_route": {}
}
}
]
}
@@ -0,0 +1,143 @@
$ uv run tools/flow.py test cases/lab01-challenge.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 기록 1 · VPN · 3.5s · 자동 답변 · 3 calls · $0.001040
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
✔ 기록 2 · 접수 · 3.0s · 자동 답변 · 3 calls · $0.000946
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 98,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 43,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000946
}
✔ 기록 3 · 다른 사람 · 3.4s · 자동 답변 · 3 calls · $0.000955
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548
}
✘ 사용량 요약 · 요청 3건 · 0.0s · $0.000000 · requests = 0 (expected 3); cost_usd = 0.0 (expected 1e-06..0.05); by_user has none of ['이준호', '박서연', '한지훈']; by_model has none of ['gpt-4.1-mini']
→ request POST http://localhost:8800/usage/summary
{}
← response HTTP 200
{
"requests": 0,
"calls": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"cost_usd": 0.0,
"by_model": {},
"by_user": {},
"by_route": {}
}
Passed 3/4
report: runs/lab01-challenge-20261028-210135.json
+197
View File
@@ -0,0 +1,197 @@
{
"cases": "lab01-challenge.json",
"webhook": "my-cost",
"passed": 4,
"total": 4,
"results": [
{
"case": "기록 1 · VPN",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 5.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
},
{
"case": "기록 2 · 접수",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.5,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 '처리 중' 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 106,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 51,
"cost_usd": 0.000126
}
}
},
"cost_usd": 0.0009588
}
},
{
"case": "기록 3 · 다른 사람",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.4,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1978,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 46,
"cost_usd": 0.0007892
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 108,
"completion_tokens": 49,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.0009528
}
},
{
"case": "사용량 요약 · 요청 3건",
"webhook": "http://localhost:8800/usage/summary",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512,
"by_model": {
"gpt-4.1-mini": {
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512
}
},
"by_user": {
"이준호": {
"calls": 3,
"prompt_tokens": 1999,
"completion_tokens": 150,
"cost_usd": 0.0010396
},
"박서연": {
"calls": 3,
"prompt_tokens": 1973,
"completion_tokens": 106,
"cost_usd": 0.0009588
},
"한지훈": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_route": {
"자동 답변": {
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512
}
}
}
}
]
}
+190
View File
@@ -0,0 +1,190 @@
$ uv run tools/flow.py push answers/75-lab01-challenge-record.json
! unpublished '75 IT helpdesk + user limit (reference)' (w3b18d2741bd22c6): it used the same webhook path
✔ pushed '75 IT helpdesk + usage record (answer)' (id waec469b37d74b05)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab01-challenge.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 기록 1 · VPN · 5.0s · 자동 답변 · 3 calls · $0.001040
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
✔ 기록 2 · 접수 · 3.5s · 자동 답변 · 3 calls · $0.000959
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 '처리 중' 상태이며, 인증서 발급 승인 대기 중입니다. 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 연락해 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1973,
"completion_tokens": 106,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 49,
"cost_usd": 0.0007948
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 111,
"completion_tokens": 51,
"cost_usd": 0.000126
}
}
},
"cost_usd": 0.0009588
}
✔ 기록 3 · 다른 사람 · 3.4s · 자동 답변 · 3 calls · $0.000953
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1978,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 46,
"cost_usd": 0.0007892
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 108,
"completion_tokens": 49,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.0009528
}
✔ 사용량 요약 · 요청 3건 · 0.0s · $0.002951
→ request POST http://localhost:8800/usage/summary
{}
← response HTTP 200
{
"requests": 3,
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512,
"by_model": {
"gpt-4.1-mini": {
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512
}
},
"by_user": {
"이준호": {
"calls": 3,
"prompt_tokens": 1999,
"completion_tokens": 150,
"cost_usd": 0.0010396
},
"박서연": {
"calls": 3,
"prompt_tokens": 1973,
"completion_tokens": 106,
"cost_usd": 0.0009588
},
"한지훈": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_route": {
"자동 답변": {
"calls": 9,
"prompt_tokens": 5950,
"completion_tokens": 357,
"cost_usd": 0.0029512
}
}
}
Passed 4/4
report: runs/lab01-challenge-20261028-211025.json
$ uv run tools/flow.py usage
requests 3 · calls 9 · tokens in 5950 / out 357 · $0.002951
model gpt-4.1-mini 9 calls · in 5950 · out 357 · $0.002951
user 이준호 3 calls · in 1999 · out 150 · $0.001040
user 박서연 3 calls · in 1973 · out 106 · $0.000959
user 한지훈 3 calls · in 1978 · out 101 · $0.000953
route 자동 답변 9 calls · in 5950 · out 357 · $0.002951
budget $1.0 / used $0.002951 · remaining $0.997049 · over False · per user 20/min · question 500 chars
@@ -0,0 +1,193 @@
{
"cases": "lab01-challenge2.json",
"webhook": "my-cost",
"passed": 1,
"total": 4,
"results": [
{
"case": "기록 API 고장(500) · 답은 나감",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"recorded = null (expected false)"
],
"seconds": 3.4,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
},
{
"case": "기록 API 느림(8초) · 답은 나감",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"recorded = null (expected false)"
],
"seconds": 11.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1968,
"completion_tokens": 109,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 55,
"cost_usd": 0.0007996
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 117,
"completion_tokens": 48,
"cost_usd": 0.0001236
}
}
},
"cost_usd": 0.0009616
}
},
{
"case": "고친 뒤 · 기록됨",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"recorded = null (expected true)"
],
"seconds": 3.5,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1978,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 46,
"cost_usd": 0.0007892
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 108,
"completion_tokens": 49,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.0009528
}
},
{
"case": "요약 · 기록된 요청은 1건",
"webhook": "http://localhost:8800/usage/summary",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 1,
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528,
"by_model": {
"gpt-4.1-mini": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_user": {
"한지훈": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_route": {
"자동 답변": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
}
}
}
]
}
@@ -0,0 +1,232 @@
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 9}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 9
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "error", "slow_seconds": 8}
→ request POST http://localhost:8800/admin/fault
{
"usage": "error"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "error",
"slow_seconds": 8
}
✘ 기록 API 고장(500) · 답은 나감 · 3.4s · 자동 답변 · 3 calls · $0.001040 · recorded = null (expected false)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "slow", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "slow",
"slow_seconds": 8
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "slow",
"slow_seconds": 8.0
}
✘ 기록 API 느림(8초) · 답은 나감 · 11.2s · 자동 답변 · 3 calls · $0.000962 · recorded = null (expected false)
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1968,
"completion_tokens": 109,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 55,
"cost_usd": 0.0007996
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 117,
"completion_tokens": 48,
"cost_usd": 0.0001236
}
}
},
"cost_usd": 0.0009616
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8.0
}
✘ 고친 뒤 · 기록됨 · 3.5s · 자동 답변 · 3 calls · $0.000953 · recorded = null (expected true)
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1978,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 46,
"cost_usd": 0.0007892
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 108,
"completion_tokens": 49,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.0009528
}
✔ 요약 · 기록된 요청은 1건 · 0.0s · $0.000953
→ request POST http://localhost:8800/usage/summary
{}
← response HTTP 200
{
"requests": 1,
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528,
"by_model": {
"gpt-4.1-mini": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_user": {
"한지훈": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
},
"by_route": {
"자동 답변": {
"calls": 3,
"prompt_tokens": 1978,
"completion_tokens": 101,
"cost_usd": 0.0009528
}
}
}
Passed 1/4
report: runs/lab01-challenge2-20261028-211043.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8.0
}
@@ -0,0 +1,190 @@
{
"cases": "lab01-challenge2.json",
"webhook": "my-cost",
"passed": 4,
"total": 4,
"results": [
{
"case": "기록 API 고장(500) · 답은 나감",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.4,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396,
"recorded": false
}
},
{
"case": "기록 API 느림(8초) · 답은 나감",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 6.1,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1967,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 54,
"cost_usd": 0.000798
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 48,
"cost_usd": 0.0001232
}
}
},
"cost_usd": 0.0009596,
"recorded": false
}
},
{
"case": "고친 뒤 · 기록됨",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.7,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548,
"recorded": true
}
},
{
"case": "요약 · 기록된 요청은 1건",
"webhook": "http://localhost:8800/usage/summary",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 1,
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548,
"by_model": {
"gpt-4.1-mini": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
},
"by_user": {
"한지훈": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
},
"by_route": {
"자동 답변": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
}
}
}
]
}
+250
View File
@@ -0,0 +1,250 @@
$ uv run tools/flow.py push answers/75-lab01-challenge2-safe-record.json
! unpublished '75 IT helpdesk + usage record (answer)' (waec469b37d74b05): it used the same webhook path
✔ pushed '75 IT helpdesk + safe record (answer)' (id w2ae10aa66addee4)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 3}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 3
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8.0
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "error", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "error"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "error",
"slow_seconds": 8.0
}
✔ 기록 API 고장(500) · 답은 나감 · 4.4s · 자동 답변 · 3 calls · $0.001040
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속이 자주 끊길 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 IT 포털에서 확인하시기 바랍니다. 인증서는 만료 30일 전부터 재발급 신청이 가능합니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1999,
"completion_tokens": 150,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 78,
"cost_usd": 0.0008372
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 140,
"completion_tokens": 66,
"cost_usd": 0.0001616
}
}
},
"cost_usd": 0.0010396,
"recorded": false
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "slow", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "slow",
"slow_seconds": 8
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "slow",
"slow_seconds": 8.0
}
✔ 기록 API 느림(8초) · 답은 나감 · 6.1s · 자동 답변 · 3 calls · $0.000960
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1967,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 54,
"cost_usd": 0.000798
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 48,
"cost_usd": 0.0001232
}
}
},
"cost_usd": 0.0009596,
"recorded": false
}
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8.0
}
✔ 고친 뒤 · 기록됨 · 3.7s · 자동 답변 · 3 calls · $0.000955
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548,
"recorded": true
}
✔ 요약 · 기록된 요청은 1건 · 0.0s · $0.000955
→ request POST http://localhost:8800/usage/summary
{}
← response HTTP 200
{
"requests": 1,
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548,
"by_model": {
"gpt-4.1-mini": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
},
"by_user": {
"한지훈": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
},
"by_route": {
"자동 답변": {
"calls": 3,
"prompt_tokens": 1979,
"completion_tokens": 102,
"cost_usd": 0.0009548
}
}
}
Passed 4/4
report: runs/lab01-challenge2-20261028-211109.json
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "slow_seconds": 8.0}
→ request POST http://localhost:8800/admin/fault
{
"usage": "ok"
}
← response HTTP 200
{
"faq": "ok",
"tickets": "ok",
"prompts": "ok",
"usage": "ok",
"slow_seconds": 8.0
}
$ uv run tools/flow.py test cases/lab01-challenge.json
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 3}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 기록 1 · VPN · 3.7s · 자동 답변 · 3 calls · $0.001040
✔ 기록 2 · 접수 · 3.4s · 자동 답변 · 3 calls · $0.000978
✔ 기록 3 · 다른 사람 · 3.2s · 자동 답변 · 3 calls · $0.000953
✔ 사용량 요약 · 요청 3건 · 0.0s · $0.002970
Passed 4/4
report: runs/lab01-challenge-20261028-211120.json
+245
View File
@@ -0,0 +1,245 @@
{
"cases": "lab02.json",
"webhook": "my-cost",
"passed": 2,
"total": 5,
"results": [
{
"case": "예제 1 · VPN · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"usage.by_step.answer.prompt_tokens = 1781 (expected 1..600)"
],
"seconds": 3.4,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인하시기 바랍니다. 인증서가 만료된 경우 IT 포털에서 재발급받으실 수 있습니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1986,
"completion_tokens": 133,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 65,
"cost_usd": 0.0008164
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 127,
"completion_tokens": 62,
"cost_usd": 0.00015
}
}
},
"cost_usd": 0.0010072,
"recorded": true
}
},
{
"case": "예제 1 · 접수 · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"usage.by_step.answer.prompt_tokens = 1791 (expected 1..600)"
],
"seconds": 3.5,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 연락 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1969,
"completion_tokens": 93,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 45,
"cost_usd": 0.0007884
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 107,
"completion_tokens": 42,
"cost_usd": 0.00011
}
}
},
"cost_usd": 0.0009364,
"recorded": true
}
},
{
"case": "예제 1 · 비밀번호 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.5,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548,
"recorded": true
}
},
{
"case": "예제 2 · 분류는 작은 모델",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"usage.by_step.classify.model = gpt-4.1-mini (expected gpt-5.4-nano)"
],
"seconds": 3.2,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "회의실 화면 공유 문제는 IT 문의로 연락 주시면 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1883,
"completion_tokens": 48,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1719,
"completion_tokens": 24,
"cost_usd": 0.000726
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 86,
"completion_tokens": 18,
"cost_usd": 6.32e-05
}
}
},
"cost_usd": 0.00083,
"recorded": true
}
},
{
"case": "예제 2 · 프린터 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.5,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1967,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 54,
"cost_usd": 0.000798
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 48,
"cost_usd": 0.0001232
}
}
},
"cost_usd": 0.0009596,
"recorded": true
}
}
]
}
+205
View File
@@ -0,0 +1,205 @@
$ uv run tools/flow.py test cases/lab02.json -v
POST http://localhost:5678/webhook/my-cost · 5 case(s) × 1
✘ 예제 1 · VPN · 보내는 양 줄이기 · 3.4s · 자동 답변 · 3 calls · $0.001007 · usage.by_step.answer.prompt_tokens = 1781 (expected 1..600)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류가 발생하면 VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인하시기 바랍니다. 인증서가 만료된 경우 IT 포털에서 재발급받으실 수 있습니다. 추가 도움이 필요하시면 IT 문의로 연락해 주십시오.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1986,
"completion_tokens": 133,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1781,
"completion_tokens": 65,
"cost_usd": 0.0008164
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 127,
"completion_tokens": 62,
"cost_usd": 0.00015
}
}
},
"cost_usd": 0.0010072,
"recorded": true
}
✘ 예제 1 · 접수 · 보내는 양 줄이기 · 3.5s · 자동 답변 · 3 calls · $0.000936 · usage.by_step.answer.prompt_tokens = 1791 (expected 1..600)
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1002는 현재 인증서 발급 승인 대기 중이며, 완료 예정일은 10월 20일입니다. 추가 문의 사항이 있으시면 연락 주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1969,
"completion_tokens": 93,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1791,
"completion_tokens": 45,
"cost_usd": 0.0007884
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 107,
"completion_tokens": 42,
"cost_usd": 0.00011
}
}
},
"cost_usd": 0.0009364,
"recorded": true
}
✔ 예제 1 · 비밀번호 · 품질 그대로 · 3.5s · 자동 답변 · 3 calls · $0.000955
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 IT 문의로 연락해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1979,
"completion_tokens": 102,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1789,
"completion_tokens": 47,
"cost_usd": 0.0007908
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 109,
"completion_tokens": 49,
"cost_usd": 0.000122
}
}
},
"cost_usd": 0.0009548,
"recorded": true
}
✘ 예제 2 · 분류는 작은 모델 · 3.2s · 자동 답변 · 3 calls · $0.000830 · usage.by_step.classify.model = gpt-4.1-mini (expected gpt-5.4-nano)
→ request POST /webhook/my-cost
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "회의실 화면 공유 문제는 IT 문의로 연락 주시면 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1883,
"completion_tokens": 48,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1719,
"completion_tokens": 24,
"cost_usd": 0.000726
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 86,
"completion_tokens": 18,
"cost_usd": 6.32e-05
}
}
},
"cost_usd": 0.00083,
"recorded": true
}
✔ 예제 2 · 프린터 · 품질 그대로 · 3.5s · 자동 답변 · 3 calls · $0.000960
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 IT 담당자에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 1967,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 1779,
"completion_tokens": 54,
"cost_usd": 0.000798
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 48,
"cost_usd": 0.0001232
}
}
},
"cost_usd": 0.0009596,
"recorded": true
}
Passed 2/5
report: runs/lab02-20261028-210311.json
+241
View File
@@ -0,0 +1,241 @@
{
"cases": "lab02.json",
"webhook": "my-cost",
"passed": 4,
"total": 5,
"results": [
{
"case": "예제 1 · VPN · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.7,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류가 발생할 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 주시기 바랍니다. 현재 관련된 접수된 티켓은 없습니다. 추가 도움이 필요하시면 언제든지 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 361,
"completion_tokens": 126,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 118,
"completion_tokens": 64,
"cost_usd": 0.0001496
}
}
},
"cost_usd": 0.000346,
"recorded": true
}
},
{
"case": "예제 1 · 접수 · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 언제든지 알려주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 330,
"completion_tokens": 95,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 45,
"cost_usd": 0.0001328
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 107,
"completion_tokens": 44,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000284,
"recorded": true
}
},
{
"case": "예제 1 · 비밀번호 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.1,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 355,
"completion_tokens": 95,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 105,
"completion_tokens": 46,
"cost_usd": 0.0001156
}
}
},
"cost_usd": 0.000294,
"recorded": true
}
},
{
"case": "예제 2 · 분류는 작은 모델",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"usage.by_step.classify.model = gpt-4.1-mini (expected gpt-5.4-nano)"
],
"seconds": 2.7,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 278,
"completion_tokens": 45,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 76,
"completion_tokens": 25,
"cost_usd": 7.04e-05
}
}
},
"cost_usd": 0.0001832,
"recorded": true
}
},
{
"case": "예제 2 · 프린터 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 352,
"completion_tokens": 107,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 47,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.000312,
"recorded": true
}
}
]
}
+215
View File
@@ -0,0 +1,215 @@
$ uv run tools/flow.py push reference/lab02-example1-lean.json
! unpublished '75 IT helpdesk + safe record (answer)' (w2ae10aa66addee4): it used the same webhook path
✔ pushed '75 IT helpdesk + lean prompt (reference)' (id wd453c4a61b08d35)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab02.json -v
POST http://localhost:5678/webhook/my-cost · 5 case(s) × 1
✔ 예제 1 · VPN · 보내는 양 줄이기 · 4.7s · 자동 답변 · 3 calls · $0.000346
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류가 발생할 경우, VPN 클라이언트를 최신 버전으로 업데이트하고 인증서 만료 여부를 확인한 후 다시 접속해 주시기 바랍니다. 현재 관련된 접수된 티켓은 없습니다. 추가 도움이 필요하시면 언제든지 문의해 주시기 바랍니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 361,
"completion_tokens": 126,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 118,
"completion_tokens": 64,
"cost_usd": 0.0001496
}
}
},
"cost_usd": 0.000346,
"recorded": true
}
✔ 예제 1 · 접수 · 보내는 양 줄이기 · 3.2s · 자동 답변 · 3 calls · $0.000284
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 인증서 발급 승인 대기 중이며, 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 언제든지 알려주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 330,
"completion_tokens": 95,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 71,
"completion_tokens": 6,
"cost_usd": 3.8e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 45,
"cost_usd": 0.0001328
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 107,
"completion_tokens": 44,
"cost_usd": 0.0001132
}
}
},
"cost_usd": 0.000284,
"recorded": true
}
✔ 예제 1 · 비밀번호 · 품질 그대로 · 3.1s · 자동 답변 · 3 calls · $0.000294
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주시기 바랍니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 355,
"completion_tokens": 95,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 81,
"completion_tokens": 6,
"cost_usd": 4.2e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 105,
"completion_tokens": 46,
"cost_usd": 0.0001156
}
}
},
"cost_usd": 0.000294,
"recorded": true
}
✘ 예제 2 · 분류는 작은 모델 · 2.7s · 자동 답변 · 3 calls · $0.000183 · usage.by_step.classify.model = gpt-4.1-mini (expected gpt-5.4-nano)
→ request POST /webhook/my-cost
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "죄송합니다만, 해당 정보를 찾지 못했습니다. 담당 직원이 곧 도움을 드리겠습니다. 감사합니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 278,
"completion_tokens": 45,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 78,
"completion_tokens": 6,
"cost_usd": 4.08e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 76,
"completion_tokens": 25,
"cost_usd": 7.04e-05
}
}
},
"cost_usd": 0.0001832,
"recorded": true
}
✔ 예제 2 · 프린터 · 품질 그대로 · 3.2s · 자동 답변 · 3 calls · $0.000312
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 후 다시 출력해 보시기 바랍니다. 추가 도움이 필요하시면 담당 직원에게 문의해 주시기 바랍니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 352,
"completion_tokens": 107,
"by_step": {
"classify": {
"model": "gpt-4.1-mini",
"prompt_tokens": 72,
"completion_tokens": 6,
"cost_usd": 3.84e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
},
"review": {
"model": "gpt-4.1-mini",
"prompt_tokens": 116,
"completion_tokens": 47,
"cost_usd": 0.0001216
}
}
},
"cost_usd": 0.000312,
"recorded": true
}
Passed 4/5
report: runs/lab02-20261028-210339.json
$ uv run tools/flow.py diff answers/75-lab01-challenge2-safe-record.json reference/lab02-example1-lean.json
75 IT helpdesk + safe record (answer) → 75 IT helpdesk + lean prompt (reference)
~ 'Answer' parameters.jsonBody: changed (3491 → 822 chars)
1 change(s)
+239
View File
@@ -0,0 +1,239 @@
{
"cases": "lab02.json",
"webhook": "my-cost",
"passed": 5,
"total": 5,
"results": [
{
"case": "예제 1 · VPN · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 4.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 359,
"completion_tokens": 123,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 117,
"completion_tokens": 59,
"cost_usd": 9.715e-05
}
}
},
"cost_usd": 0.00027815,
"recorded": true
}
},
{
"case": "예제 1 · 접수 · 보내는 양 줄이기",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 317,
"completion_tokens": 79,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 95,
"completion_tokens": 37,
"cost_usd": 6.525e-05
}
}
},
"cost_usd": 0.00020445,
"recorded": true
}
},
{
"case": "예제 1 · 비밀번호 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 354,
"completion_tokens": 99,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 44,
"cost_usd": 0.000138
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 105,
"completion_tokens": 47,
"cost_usd": 7.975e-05
}
}
},
"cost_usd": 0.00024375,
"recorded": true
}
},
{
"case": "예제 2 · 분류는 작은 모델",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.7,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "죄송합니다. 요청하신 내용을 찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 276,
"completion_tokens": 47,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 75,
"completion_tokens": 25,
"cost_usd": 4.625e-05
}
}
},
"cost_usd": 0.00014365,
"recorded": true
}
},
{
"case": "예제 2 · 프린터 · 품질 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.7,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인한 뒤 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 350,
"completion_tokens": 119,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 115,
"completion_tokens": 57,
"cost_usd": 9.425e-05
}
}
},
"cost_usd": 0.00027045,
"recorded": true
}
}
]
}
+218
View File
@@ -0,0 +1,218 @@
$ uv run tools/flow.py push reference/lab02-example2-models.json
! unpublished '75 IT helpdesk + lean prompt (reference)' (wd453c4a61b08d35): it used the same webhook path
✔ pushed '75 IT helpdesk + small models (reference)' (id w5619722541404a5)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab02.json -v
POST http://localhost:5678/webhook/my-cost · 5 case(s) × 1
✔ 예제 1 · VPN · 보내는 양 줄이기 · 4.3s · 자동 답변 · 3 calls · $0.000278
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 359,
"completion_tokens": 123,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 117,
"completion_tokens": 59,
"cost_usd": 9.715e-05
}
}
},
"cost_usd": 0.00027815,
"recorded": true
}
✔ 예제 1 · 접수 · 보내는 양 줄이기 · 3.1s · 자동 답변 · 3 calls · $0.000204
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 317,
"completion_tokens": 79,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 95,
"completion_tokens": 37,
"cost_usd": 6.525e-05
}
}
},
"cost_usd": 0.00020445,
"recorded": true
}
✔ 예제 1 · 비밀번호 · 품질 그대로 · 3.6s · 자동 답변 · 3 calls · $0.000244
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 354,
"completion_tokens": 99,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 44,
"cost_usd": 0.000138
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 105,
"completion_tokens": 47,
"cost_usd": 7.975e-05
}
}
},
"cost_usd": 0.00024375,
"recorded": true
}
✔ 예제 2 · 분류는 작은 모델 · 2.7s · 자동 답변 · 3 calls · $0.000144
→ request POST /webhook/my-cost
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "죄송합니다. 요청하신 내용을 찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 276,
"completion_tokens": 47,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 75,
"completion_tokens": 25,
"cost_usd": 4.625e-05
}
}
},
"cost_usd": 0.00014365,
"recorded": true
}
✔ 예제 2 · 프린터 · 품질 그대로 · 3.7s · 자동 답변 · 3 calls · $0.000270
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인한 뒤 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 350,
"completion_tokens": 119,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 115,
"completion_tokens": 57,
"cost_usd": 9.425e-05
}
}
},
"cost_usd": 0.00027045,
"recorded": true
}
Passed 5/5
report: runs/lab02-20261028-210408.json
$ uv run tools/flow.py test cases/lab01.json
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ VPN · 비용과 단계별 사용량 · 3.3s · 자동 답변 · 3 calls · $0.000278
✔ 접수 현황 · 비용 · 3.5s · 자동 답변 · 3 calls · $0.000204
✔ 빈 질문은 LLM 없이 · 0.0s
Passed 3/3
report: runs/lab01-20261028-210415.json
@@ -0,0 +1,239 @@
{
"cases": "lab02.json",
"webhook": "nano-answer",
"passed": 5,
"total": 5,
"results": [
{
"case": "예제 1 · VPN · 보내는 양 줄이기",
"webhook": "nano-answer",
"run": 1,
"passed": true,
"problems": [],
"seconds": 5.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "FAQ에 따르면 VPN 접속이 끊길 때는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 ticket_id가 없어 상태를 확인할 수 없으며, 요청하신 티켓 기록은 찾지 못했습니다. 담당 직원이 추가로 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 383,
"completion_tokens": 176,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 164,
"completion_tokens": 84,
"cost_usd": 0.0001378
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 142,
"completion_tokens": 84,
"cost_usd": 0.0001334
}
}
},
"cost_usd": 0.0002966,
"recorded": true
}
},
{
"case": "예제 1 · 접수 · 보내는 양 줄이기",
"webhook": "nano-answer",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.8,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002의 처리 상태는 **처리 중**이며, **인증서 발급 승인 대기 중**입니다. 또한 **10월 20일 완료 예정**으로 기록되어 있습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 325,
"completion_tokens": 100,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 151,
"completion_tokens": 46,
"cost_usd": 8.77e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 104,
"completion_tokens": 46,
"cost_usd": 7.83e-05
}
}
},
"cost_usd": 0.00019,
"recorded": true
}
},
{
"case": "예제 1 · 비밀번호 · 품질 그대로",
"webhook": "nano-answer",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.7,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 접수 건은 찾지 못했습니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 346,
"completion_tokens": 88,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 168,
"completion_tokens": 40,
"cost_usd": 8.36e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 98,
"completion_tokens": 40,
"cost_usd": 6.96e-05
}
}
},
"cost_usd": 0.0001792,
"recorded": true
}
},
{
"case": "예제 2 · 분류는 작은 모델",
"webhook": "nano-answer",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.2,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "제공된 FAQ와 접수 건 기록에서 “회의실 화면 공유” 관련 안내를 찾지 못했습니다. 또한 해당 티켓 정보가 없어 상태를 확인할 수 없습니다. 담당자에게 확인해 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 307,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 123,
"completion_tokens": 50,
"cost_usd": 8.71e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 107,
"completion_tokens": 50,
"cost_usd": 8.39e-05
}
}
},
"cost_usd": 0.0001964,
"recorded": true
}
},
{
"case": "예제 2 · 프린터 · 품질 그대로",
"webhook": "nano-answer",
"run": 1,
"passed": true,
"problems": [],
"seconds": 3.3,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 접수 건은 찾지 못했습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 340,
"completion_tokens": 109,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 163,
"completion_tokens": 48,
"cost_usd": 9.26e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 106,
"completion_tokens": 53,
"cost_usd": 8.745e-05
}
}
},
"cost_usd": 0.00020425,
"recorded": true
}
}
]
}
@@ -0,0 +1,233 @@
$ uv run tools/flow.py push reference/lab02-example3-nano-answer.json
✔ pushed '75 IT helpdesk, answer on nano too (compare)' (id wf141d1eb1ccfef8)
POST http://localhost:5678/webhook/nano-answer
$ uv run tools/flow.py test cases/lab02.json --webhook nano-answer -v
POST http://localhost:5678/webhook/nano-answer · 5 case(s) × 1
✔ 예제 1 · VPN · 보내는 양 줄이기 · 5.0s · 자동 답변 · 3 calls · $0.000297
→ request POST /webhook/nano-answer
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "FAQ에 따르면 VPN 접속이 끊길 때는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속해 보시기 바랍니다. 접수 건은 ticket_id가 없어 상태를 확인할 수 없으며, 요청하신 티켓 기록은 찾지 못했습니다. 담당 직원이 추가로 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 383,
"completion_tokens": 176,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 164,
"completion_tokens": 84,
"cost_usd": 0.0001378
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 142,
"completion_tokens": 84,
"cost_usd": 0.0001334
}
}
},
"cost_usd": 0.0002966,
"recorded": true
}
✔ 예제 1 · 접수 · 보내는 양 줄이기 · 2.8s · 자동 답변 · 3 calls · $0.000190
→ request POST /webhook/nano-answer
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002의 처리 상태는 **처리 중**이며, **인증서 발급 승인 대기 중**입니다. 또한 **10월 20일 완료 예정**으로 기록되어 있습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 325,
"completion_tokens": 100,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 151,
"completion_tokens": 46,
"cost_usd": 8.77e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 104,
"completion_tokens": 46,
"cost_usd": 7.83e-05
}
}
},
"cost_usd": 0.00019,
"recorded": true
}
✔ 예제 1 · 비밀번호 · 품질 그대로 · 3.7s · 자동 답변 · 3 calls · $0.000179
→ request POST /webhook/nano-answer
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 접수 건은 찾지 못했습니다.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 346,
"completion_tokens": 88,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 168,
"completion_tokens": 40,
"cost_usd": 8.36e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 98,
"completion_tokens": 40,
"cost_usd": 6.96e-05
}
}
},
"cost_usd": 0.0001792,
"recorded": true
}
✔ 예제 2 · 분류는 작은 모델 · 3.2s · 자동 답변 · 3 calls · $0.000196
→ request POST /webhook/nano-answer
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "제공된 FAQ와 접수 건 기록에서 “회의실 화면 공유” 관련 안내를 찾지 못했습니다. 또한 해당 티켓 정보가 없어 상태를 확인할 수 없습니다. 담당자에게 확인해 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 307,
"completion_tokens": 108,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 123,
"completion_tokens": 50,
"cost_usd": 8.71e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 107,
"completion_tokens": 50,
"cost_usd": 8.39e-05
}
}
},
"cost_usd": 0.0001964,
"recorded": true
}
✔ 예제 2 · 프린터 · 품질 그대로 · 3.3s · 자동 답변 · 3 calls · $0.000204
→ request POST /webhook/nano-answer
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 출력이 안 될 때는 프린터 전원과 네트워크 연결을 확인한 뒤, 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 접수 건은 찾지 못했습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 340,
"completion_tokens": 109,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-5.4-nano",
"prompt_tokens": 163,
"completion_tokens": 48,
"cost_usd": 9.26e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 106,
"completion_tokens": 53,
"cost_usd": 8.745e-05
}
}
},
"cost_usd": 0.00020425,
"recorded": true
}
Passed 5/5
report: runs/lab02-20261028-210444.json
$ uv run tools/flow.py test cases/lab01-mix.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ FAQ · VPN · 3.6s · 자동 답변 · 3 calls · $0.000278
✔ FAQ · 프린터 · 3.1s · 자동 답변 · 3 calls · $0.000270
✔ FAQ · 비밀번호 · 3.2s · 자동 답변 · 3 calls · $0.000241
✔ FAQ · 메일 · 3.1s · 자동 답변 · 3 calls · $0.000297
✔ 접수 · T-1002 · 3.0s · 자동 답변 · 3 calls · $0.000204
✔ 접수 · T-1001 · 3.3s · 자동 답변 · 3 calls · $0.000195
✔ 규칙에 없는 질문 · 2.8s · 자동 답변 · 3 calls · $0.000144
Passed 7/7
report: runs/lab01-mix-20261028-210506.json
$ uv run tools/flow.py test cases/lab01-mix.json --webhook nano-answer
POST http://localhost:5678/webhook/nano-answer · 7 case(s) × 1
✔ FAQ · VPN · 3.7s · 자동 답변 · 3 calls · $0.000217
✔ FAQ · 프린터 · 3.1s · 자동 답변 · 3 calls · $0.000245
✔ FAQ · 비밀번호 · 3.0s · 자동 답변 · 3 calls · $0.000179
✔ FAQ · 메일 · 3.3s · 자동 답변 · 3 calls · $0.000209
✔ 접수 · T-1002 · 3.3s · 자동 답변 · 3 calls · $0.000190
✔ 접수 · T-1001 · 3.2s · 자동 답변 · 3 calls · $0.000153
✔ 규칙에 없는 질문 · 3.2s · 자동 답변 · 3 calls · $0.000196
Passed 7/7
report: runs/lab01-mix-20261028-210529.json
@@ -0,0 +1,350 @@
{
"cases": "lab02-challenge.json",
"webhook": "my-cost",
"passed": 0,
"total": 7,
"results": [
{
"case": "faq-vpn · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)",
"cost_usd = 0.00027815 (expected 1e-06..0.0002)"
],
"seconds": 3.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 359,
"completion_tokens": 123,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 117,
"completion_tokens": 59,
"cost_usd": 9.715e-05
}
}
},
"cost_usd": 0.00027815,
"recorded": true
}
},
{
"case": "faq-password · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)",
"cost_usd = 0.0002407 (expected 1e-06..0.0002)"
],
"seconds": 3.6,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 353,
"completion_tokens": 97,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 104,
"completion_tokens": 46,
"cost_usd": 7.83e-05
}
}
},
"cost_usd": 0.0002407,
"recorded": true
}
},
{
"case": "faq-printer · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)",
"cost_usd = 0.0002674 (expected 1e-06..0.0002)"
],
"seconds": 3.5,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인한 뒤 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 349,
"completion_tokens": 117,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 114,
"completion_tokens": 56,
"cost_usd": 9.28e-05
}
}
},
"cost_usd": 0.0002674,
"recorded": true
}
},
{
"case": "faq-mail · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)",
"cost_usd = 0.00029665 (expected 1e-06..0.0002)"
],
"seconds": 3.5,
"status": 200,
"request": {
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
},
"response": {
"route": "자동 답변",
"answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-005"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 364,
"completion_tokens": 135,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 73,
"completion_tokens": 8,
"cost_usd": 2.46e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 167,
"completion_tokens": 62,
"cost_usd": 0.000166
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 124,
"completion_tokens": 65,
"cost_usd": 0.00010605
}
}
},
"cost_usd": 0.00029665,
"recorded": true
}
},
{
"case": "ticket-own · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)",
"cost_usd = 0.000238 (expected 1e-06..0.0002)"
],
"seconds": 4.0,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 알려주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 328,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 45,
"cost_usd": 0.0001328
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 106,
"completion_tokens": 48,
"cost_usd": 8.12e-05
}
}
},
"cost_usd": 0.000238,
"recorded": true
}
},
{
"case": "ticket-done · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 1)"
],
"seconds": 3.2,
"status": 200,
"request": {
"question": "T-1001 어떻게 됐나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체했습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 297,
"completion_tokens": 55,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 143,
"completion_tokens": 22,
"cost_usd": 9.24e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 83,
"completion_tokens": 25,
"cost_usd": 4.785e-05
}
}
},
"cost_usd": 0.00016445,
"recorded": true
}
},
{
"case": "규칙으로 모르는 질문은 분류 + 답",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"llm_calls = 3 (expected 2)"
],
"seconds": 3.1,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "죄송합니다. 요청하신 내용을 찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 276,
"completion_tokens": 47,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 75,
"completion_tokens": 25,
"cost_usd": 4.625e-05
}
}
},
"cost_usd": 0.00014365,
"recorded": true
}
}
]
}
@@ -0,0 +1,285 @@
$ uv run tools/flow.py test cases/lab02-challenge.json -v
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✘ faq-vpn · LLM 한 번 · $0.0002 이하 · 3.0s · 자동 답변 · 3 calls · $0.000278 · llm_calls = 3 (expected 1); cost_usd = 0.00027815 (expected 1e-06..0.0002)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 359,
"completion_tokens": 123,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 117,
"completion_tokens": 59,
"cost_usd": 9.715e-05
}
}
},
"cost_usd": 0.00027815,
"recorded": true
}
✘ faq-password · LLM 한 번 · $0.0002 이하 · 3.6s · 자동 답변 · 3 calls · $0.000241 · llm_calls = 3 (expected 1); cost_usd = 0.0002407 (expected 1e-06..0.0002)
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 ‘비밀번호 찾기’에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 353,
"completion_tokens": 97,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 80,
"completion_tokens": 8,
"cost_usd": 2.6e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 104,
"completion_tokens": 46,
"cost_usd": 7.83e-05
}
}
},
"cost_usd": 0.0002407,
"recorded": true
}
✘ faq-printer · LLM 한 번 · $0.0002 이하 · 3.5s · 자동 답변 · 3 calls · $0.000267 · llm_calls = 3 (expected 1); cost_usd = 0.0002674 (expected 1e-06..0.0002)
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인한 뒤 인쇄 대기열을 비우고 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 349,
"completion_tokens": 117,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 114,
"completion_tokens": 56,
"cost_usd": 9.28e-05
}
}
},
"cost_usd": 0.0002674,
"recorded": true
}
✘ faq-mail · LLM 한 번 · $0.0002 이하 · 3.5s · 자동 답변 · 3 calls · $0.000297 · llm_calls = 3 (expected 1); cost_usd = 0.00029665 (expected 1e-06..0.0002)
→ request POST /webhook/my-cost
{
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-005"
],
"llm_calls": 3,
"usage": {
"prompt_tokens": 364,
"completion_tokens": 135,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 73,
"completion_tokens": 8,
"cost_usd": 2.46e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 167,
"completion_tokens": 62,
"cost_usd": 0.000166
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 124,
"completion_tokens": 65,
"cost_usd": 0.00010605
}
}
},
"cost_usd": 0.00029665,
"recorded": true
}
✘ ticket-own · LLM 한 번 · $0.0002 이하 · 4.0s · 자동 답변 · 3 calls · $0.000238 · llm_calls = 3 (expected 1); cost_usd = 0.000238 (expected 1e-06..0.0002)
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다. 추가 문의 사항이 있으시면 알려주시기 바랍니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 328,
"completion_tokens": 101,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 70,
"completion_tokens": 8,
"cost_usd": 2.4e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 45,
"cost_usd": 0.0001328
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 106,
"completion_tokens": 48,
"cost_usd": 8.12e-05
}
}
},
"cost_usd": 0.000238,
"recorded": true
}
✘ ticket-done · LLM 한 번 · $0.0002 이하 · 3.2s · 자동 답변 · 3 calls · $0.000164 · llm_calls = 3 (expected 1)
→ request POST /webhook/my-cost
{
"question": "T-1001 어떻게 됐나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체했습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 297,
"completion_tokens": 55,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 71,
"completion_tokens": 8,
"cost_usd": 2.42e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 143,
"completion_tokens": 22,
"cost_usd": 9.24e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 83,
"completion_tokens": 25,
"cost_usd": 4.785e-05
}
}
},
"cost_usd": 0.00016445,
"recorded": true
}
✘ 규칙으로 모르는 질문은 분류 + 답 · 3.1s · 자동 답변 · 3 calls · $0.000144 · llm_calls = 3 (expected 2)
→ request POST /webhook/my-cost
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "죄송합니다. 요청하신 내용을 찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 3,
"usage": {
"prompt_tokens": 276,
"completion_tokens": 47,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
},
"review": {
"model": "gpt-5.4-nano",
"prompt_tokens": 75,
"completion_tokens": 25,
"cost_usd": 4.625e-05
}
}
},
"cost_usd": 0.00014365,
"recorded": true
}
Passed 0/7
report: runs/lab02-challenge-20261028-210554.json
+253
View File
@@ -0,0 +1,253 @@
{
"cases": "lab02-challenge.json",
"webhook": "my-cost",
"passed": 7,
"total": 7,
"results": [
{
"case": "faq-vpn · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.8,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"recorded": true
}
},
{
"case": "faq-password · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 44,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 44,
"cost_usd": 0.000138
}
}
},
"cost_usd": 0.000138,
"recorded": true
}
},
{
"case": "faq-printer · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 53,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
}
}
},
"cost_usd": 0.0001504,
"recorded": true
}
},
{
"case": "faq-mail · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.5,
"status": 200,
"request": {
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
},
"response": {
"route": "자동 답변",
"answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-005"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 167,
"completion_tokens": 62,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 167,
"completion_tokens": 62,
"cost_usd": 0.000166
}
}
},
"cost_usd": 0.000166,
"recorded": true
}
},
{
"case": "ticket-own · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.4,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 152,
"completion_tokens": 34,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
}
}
},
"cost_usd": 0.0001152,
"recorded": true
}
},
{
"case": "ticket-done · LLM 한 번 · $0.0002 이하",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"question": "T-1001 어떻게 됐나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체했습니다. 추가 문의 사항이 있으시면 말씀해 주세요.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 143,
"completion_tokens": 32,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 143,
"completion_tokens": 32,
"cost_usd": 0.0001084
}
}
},
"cost_usd": 0.0001084,
"recorded": true
}
},
{
"case": "규칙으로 모르는 질문은 분류 + 답",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.5,
"status": 200,
"request": {
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"usage": {
"prompt_tokens": 201,
"completion_tokens": 22,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
}
}
},
"cost_usd": 9.74e-05,
"recorded": true
}
}
]
}
+240
View File
@@ -0,0 +1,240 @@
$ uv run tools/flow.py push answers/75-lab02-challenge-one-call.json
! unpublished '75 IT helpdesk + small models (reference)' (w5619722541404a5): it used the same webhook path
✔ pushed '75 IT helpdesk + one LLM call (answer)' (id wbf73b5f2c309119)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab02-challenge.json -v
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ faq-vpn · LLM 한 번 · $0.0002 이하 · 2.8s · 자동 답변 · 1 call · $0.000156
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"recorded": true
}
✔ faq-password · LLM 한 번 · $0.0002 이하 · 1.2s · 자동 답변 · 1 call · $0.000138
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 도움이 필요하시면 언제든 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 44,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 44,
"cost_usd": 0.000138
}
}
},
"cost_usd": 0.000138,
"recorded": true
}
✔ faq-printer · LLM 한 번 · $0.0002 이하 · 1.2s · 자동 답변 · 1 call · $0.000150
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 53,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
}
}
},
"cost_usd": 0.0001504,
"recorded": true
}
✔ faq-mail · LLM 한 번 · $0.0002 이하 · 1.5s · 자동 답변 · 1 call · $0.000166
→ request POST /webhook/my-cost
{
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "메일함 용량이 초과된 경우, 오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제하여 용량을 확보하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-005"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 167,
"completion_tokens": 62,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 167,
"completion_tokens": 62,
"cost_usd": 0.000166
}
}
},
"cost_usd": 0.000166,
"recorded": true
}
✔ ticket-own · LLM 한 번 · $0.0002 이하 · 1.4s · 자동 답변 · 1 call · $0.000115
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 152,
"completion_tokens": 34,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
}
}
},
"cost_usd": 0.0001152,
"recorded": true
}
✔ ticket-done · LLM 한 번 · $0.0002 이하 · 0.9s · 자동 답변 · 1 call · $0.000108
→ request POST /webhook/my-cost
{
"question": "T-1001 어떻게 됐나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "티켓 T-1001은 처리 완료되었으며, 새 배터리로 교체했습니다. 추가 문의 사항이 있으시면 말씀해 주세요.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 143,
"completion_tokens": 32,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 143,
"completion_tokens": 32,
"cost_usd": 0.0001084
}
}
},
"cost_usd": 0.0001084,
"recorded": true
}
✔ 규칙으로 모르는 질문은 분류 + 답 · 1.5s · 자동 답변 · 2 calls · $0.000097
→ request POST /webhook/my-cost
{
"question": "회의실 화면 공유가 안 되는데 누구에게 물어보나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "찾지 못했습니다. 담당 직원이 도움을 드리겠습니다.",
"sources": [],
"llm_calls": 2,
"usage": {
"prompt_tokens": 201,
"completion_tokens": 22,
"by_step": {
"classify": {
"model": "gpt-5.4-nano",
"prompt_tokens": 77,
"completion_tokens": 8,
"cost_usd": 2.54e-05
},
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 124,
"completion_tokens": 14,
"cost_usd": 7.2e-05
}
}
},
"cost_usd": 9.74e-05,
"recorded": true
}
Passed 7/7
report: runs/lab02-challenge-20261028-210617.json
$ uv run tools/flow.py test cases/lab02.json
POST http://localhost:5678/webhook/my-cost · 5 case(s) × 1
✔ 예제 1 · VPN · 보내는 양 줄이기 · 1.2s · 자동 답변 · 1 call · $0.000156
✔ 예제 1 · 접수 · 보내는 양 줄이기 · 1.0s · 자동 답변 · 1 call · $0.000115
✔ 예제 1 · 비밀번호 · 품질 그대로 · 1.5s · 자동 답변 · 1 call · $0.000136
✔ 예제 2 · 분류는 작은 모델 · 2.1s · 자동 답변 · 2 calls · $0.000097
✔ 예제 2 · 프린터 · 품질 그대로 · 1.2s · 자동 답변 · 1 call · $0.000152
Passed 5/5
report: runs/lab02-20261028-210624.json
$ uv run tools/flow.py test cases/lab01.json
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ VPN · 비용과 단계별 사용량 · 1.4s · 자동 답변 · 1 call · $0.000156
✔ 접수 현황 · 비용 · 1.2s · 자동 답변 · 1 call · $0.000115
✔ 빈 질문은 LLM 없이 · 0.0s
Passed 3/3
report: runs/lab01-20261028-210627.json
$ uv run tools/flow.py test cases/lab01-challenge.json
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 157}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 기록 1 · VPN · 1.2s · 자동 답변 · 1 call · $0.000156
✔ 기록 2 · 접수 · 1.0s · 자동 답변 · 1 call · $0.000115
✔ 기록 3 · 다른 사람 · 1.4s · 자동 답변 · 1 call · $0.000138
✔ 사용량 요약 · 요청 3건 · 0.0s · $0.000409
Passed 4/4
report: runs/lab01-challenge-20261028-210630.json
@@ -0,0 +1,160 @@
{
"cases": "lab02-challenge2.json",
"webhook": "my-cost",
"passed": 0,
"total": 4,
"results": [
{
"case": "긴 로그 + 끝에 질문 · 프린터",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"trimmed = null (expected true)",
"cost_usd = 0.001196 (expected 1e-06..0.0003)"
],
"seconds": 1.3,
"status": 200,
"request": {
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"recorded": true
}
},
{
"case": "앞에 질문 + 긴 로그 · VPN",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"trimmed = null (expected true)",
"cost_usd = 0.0010992 (expected 1e-06..0.0003)"
],
"seconds": 1.4,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요? 아래는 접속 프로그램 로그입니다.\n2026-10-29 08:10:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:10:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:11:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:11:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:12:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:12:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:13:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:13:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:14:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:14:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:15:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:15:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:16:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:16:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:17:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:17:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:18:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:18:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 state=reconnecting\n2026-10-29 08:19:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=58 state=reconnecting\n2026-10-29 08:19:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=59 state=reconnecting\n2026-10-29 08:20:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=60 state=reconnecting\n2026-10-29 08:20:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=61 state=reconnecting\n2026-10-29 08:21:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=62 state=reconnecting\n2026-10-29 08:21:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=63 state=reconnecting\n2026-10-29 08:22:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=64 state=reconnecting\n2026-10-29 08:22:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=65 state=reconnecting\n2026-10-29 08:23:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=66 state=reconnecting\n2026-10-29 08:23:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=67 state=reconnecting\n2026-10-29 08:24:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=68 state=reconnecting\n2026-10-29 08:24:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=69 state=reconnecting\n2026-10-29 08:25:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:25:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:26:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:26:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:27:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:27:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:28:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:28:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:29:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:29:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:30:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:30:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:31:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:31:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:32:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:32:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:33:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:33:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 sLine truncated
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 티켓이 없어 별도의 상태 안내는 어렵습니다. 추가 도움이 필요하시면 IT 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003",
"FAQ-006"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2496,
"completion_tokens": 63,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2496,
"completion_tokens": 63,
"cost_usd": 0.0010992
}
}
},
"cost_usd": 0.0010992,
"recorded": true
}
},
{
"case": "짧은 질문은 그대로",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"trimmed = null (expected false)",
"question_chars = null (expected 24)"
],
"seconds": 1.2,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"recorded": true
}
},
{
"case": "사람이 한도를 늘리면 자르지 않음",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"trimmed = null (expected false)"
],
"seconds": 1.2,
"status": 200,
"request": {
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"recorded": true
}
}
]
}
@@ -0,0 +1,159 @@
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0004088, "remaining_usd": 0.999591
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0004088,
"remaining_usd": 0.9995912,
"over": false
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✘ 긴 로그 + 끝에 질문 · 프린터 · 1.3s · 자동 답변 · 1 call · $0.001196 · trimmed = null (expected true); cost_usd = 0.001196 (expected 1e-06..0.0003)
→ request POST /webhook/my-cost
{
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"recorded": true
}
✘ 앞에 질문 + 긴 로그 · VPN · 1.4s · 자동 답변 · 1 call · $0.001099 · trimmed = null (expected true); cost_usd = 0.0010992 (expected 1e-06..0.0003)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요? 아래는 접속 프로그램 로그입니다.\n2026-10-29 08:10:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:10:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:11:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:11:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:12:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:12:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:13:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:13:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:14:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:14:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:15:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:15:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:16:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:16:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:17:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:17:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:18:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:18:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 state=reconnecting\n2026-10-29 08:19:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=58 state=reconnecting\n2026-10-29 08:19:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=59 state=reconnecting\n2026-10-29 08:20:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=60 state=reconnecting\n2026-10-29 08:20:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=61 state=reconnecting\n2026-10-29 08:21:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=62 state=reconnecting\n2026-10-29 08:21:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=63 state=reconnecting\n2026-10-29 08:22:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=64 state=reconnecting\n2026-10-29 08:22:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=65 state=reconnecting\n2026-10-29 08:23:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=66 state=reconnecting\n2026-10-29 08:23:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=67 state=reconnecting\n2026-10-29 08:24:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=68 state=reconnecting\n2026-10-29 08:24:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=69 state=reconnecting\n2026-10-29 08:25:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:25:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:26:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:26:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:27:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:27:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:28:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:28:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:29:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:29:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:30:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:30:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:31:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:31:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:32:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:32:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:33:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:33:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 sLine truncated
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 티켓이 없어 별도의 상태 안내는 어렵습니다. 추가 도움이 필요하시면 IT 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003",
"FAQ-006"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2496,
"completion_tokens": 63,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2496,
"completion_tokens": 63,
"cost_usd": 0.0010992
}
}
},
"cost_usd": 0.0010992,
"recorded": true
}
✘ 짧은 질문은 그대로 · 1.2s · 자동 답변 · 1 call · $0.000156 · trimmed = null (expected false); question_chars = null (expected 24)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"recorded": true
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 10000, "used_usd": 0.0028596, "remaining_usd": 0.9971
→ request POST http://localhost:8800/budget
{
"max_question_chars": 10000
}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 10000,
"used_usd": 0.0028596,
"remaining_usd": 0.9971404,
"over": false
}
✘ 사람이 한도를 늘리면 자르지 않음 · 1.2s · 자동 답변 · 1 call · $0.001196 · trimmed = null (expected false)
→ request POST /webhook/my-cost
{
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"recorded": true
}
Passed 0/4
report: runs/lab02-challenge2-20261028-210636.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0040556, "remaining_usd": 0.995944
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0040556,
"remaining_usd": 0.9959444,
"over": false
}
@@ -0,0 +1,157 @@
{
"cases": "lab02-challenge2.json",
"webhook": "my-cost",
"passed": 4,
"total": 4,
"results": [
{
"case": "긴 로그 + 끝에 질문 · 프린터",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.3,
"status": 200,
"request": {
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 372,
"completion_tokens": 55,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 372,
"completion_tokens": 55,
"cost_usd": 0.0002368
}
}
},
"cost_usd": 0.0002368,
"trimmed": true,
"question_chars": 6298,
"recorded": true
}
},
{
"case": "앞에 질문 + 긴 로그 · VPN",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요? 아래는 접속 프로그램 로그입니다.\n2026-10-29 08:10:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:10:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:11:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:11:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:12:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:12:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:13:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:13:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:14:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:14:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:15:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:15:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:16:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:16:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:17:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:17:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:18:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:18:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 state=reconnecting\n2026-10-29 08:19:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=58 state=reconnecting\n2026-10-29 08:19:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=59 state=reconnecting\n2026-10-29 08:20:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=60 state=reconnecting\n2026-10-29 08:20:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=61 state=reconnecting\n2026-10-29 08:21:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=62 state=reconnecting\n2026-10-29 08:21:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=63 state=reconnecting\n2026-10-29 08:22:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=64 state=reconnecting\n2026-10-29 08:22:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=65 state=reconnecting\n2026-10-29 08:23:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=66 state=reconnecting\n2026-10-29 08:23:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=67 state=reconnecting\n2026-10-29 08:24:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=68 state=reconnecting\n2026-10-29 08:24:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=69 state=reconnecting\n2026-10-29 08:25:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:25:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:26:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:26:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:27:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:27:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:28:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:28:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:29:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:29:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:30:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:30:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:31:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:31:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:32:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:32:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:33:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:33:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 sLine truncated
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하는 것을 권장합니다. 현재 접수된 티켓이 없어 상태를 확인할 수 없으며, 추가 도움이 필요하면 IT 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003",
"FAQ-006"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 394,
"completion_tokens": 64,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 394,
"completion_tokens": 64,
"cost_usd": 0.00026
}
}
},
"cost_usd": 0.00026,
"trimmed": true,
"question_chars": 6103,
"recorded": true
}
},
{
"case": "짧은 질문은 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "사람이 한도를 늘리면 자르지 않음",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"trimmed": false,
"question_chars": 6298,
"recorded": true
}
}
]
}
+184
View File
@@ -0,0 +1,184 @@
$ uv run tools/flow.py push answers/75-lab02-challenge2-long-question.json
! unpublished '75 IT helpdesk + one LLM call (answer)' (wbf73b5f2c309119): it used the same webhook path
✔ pushed '75 IT helpdesk + long question cap (answer)' (id w3d4ba9195f92a57)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0040556, "remaining_usd": 0.995944
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0040556,
"remaining_usd": 0.9959444,
"over": false
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 긴 로그 + 끝에 질문 · 프린터 · 2.3s · 자동 답변 · 1 call · $0.000237
→ request POST /webhook/my-cost
{
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 372,
"completion_tokens": 55,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 372,
"completion_tokens": 55,
"cost_usd": 0.0002368
}
}
},
"cost_usd": 0.0002368,
"trimmed": true,
"question_chars": 6298,
"recorded": true
}
✔ 앞에 질문 + 긴 로그 · VPN · 1.1s · 자동 답변 · 1 call · $0.000260
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요? 아래는 접속 프로그램 로그입니다.\n2026-10-29 08:10:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:10:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:11:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:11:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:12:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:12:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:13:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:13:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:14:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:14:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:15:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:15:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:16:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:16:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:17:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:17:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:18:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:18:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 state=reconnecting\n2026-10-29 08:19:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=58 state=reconnecting\n2026-10-29 08:19:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=59 state=reconnecting\n2026-10-29 08:20:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=60 state=reconnecting\n2026-10-29 08:20:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=61 state=reconnecting\n2026-10-29 08:21:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=62 state=reconnecting\n2026-10-29 08:21:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=63 state=reconnecting\n2026-10-29 08:22:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=64 state=reconnecting\n2026-10-29 08:22:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=65 state=reconnecting\n2026-10-29 08:23:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=66 state=reconnecting\n2026-10-29 08:23:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=67 state=reconnecting\n2026-10-29 08:24:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=68 state=reconnecting\n2026-10-29 08:24:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=69 state=reconnecting\n2026-10-29 08:25:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=40 state=reconnecting\n2026-10-29 08:25:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=41 state=reconnecting\n2026-10-29 08:26:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=42 state=reconnecting\n2026-10-29 08:26:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=43 state=reconnecting\n2026-10-29 08:27:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=44 state=reconnecting\n2026-10-29 08:27:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=45 state=reconnecting\n2026-10-29 08:28:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=46 state=reconnecting\n2026-10-29 08:28:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=47 state=reconnecting\n2026-10-29 08:29:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=48 state=reconnecting\n2026-10-29 08:29:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=49 state=reconnecting\n2026-10-29 08:30:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=50 state=reconnecting\n2026-10-29 08:30:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=51 state=reconnecting\n2026-10-29 08:31:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=52 state=reconnecting\n2026-10-29 08:31:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=3 rtt_ms=53 state=reconnecting\n2026-10-29 08:32:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=4 rtt_ms=54 state=reconnecting\n2026-10-29 08:32:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=0 rtt_ms=55 state=reconnecting\n2026-10-29 08:33:10 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=1 rtt_ms=56 state=reconnecting\n2026-10-29 08:33:40 vpnclient[812]: tunnel=corp-gw-2 keepalive missed=2 rtt_ms=57 sLine truncated
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하는 것을 권장합니다. 현재 접수된 티켓이 없어 상태를 확인할 수 없으며, 추가 도움이 필요하면 IT 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003",
"FAQ-006"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 394,
"completion_tokens": 64,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 394,
"completion_tokens": 64,
"cost_usd": 0.00026
}
}
},
"cost_usd": 0.00026,
"trimmed": true,
"question_chars": 6103,
"recorded": true
}
✔ 짧은 질문은 그대로 · 1.1s · 자동 답변 · 1 call · $0.000141
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 10000, "used_usd": 0.0046936, "remaining_usd": 0.9953
→ request POST http://localhost:8800/budget
{
"max_question_chars": 10000
}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 10000,
"used_usd": 0.0046936,
"remaining_usd": 0.9953064,
"over": false
}
✔ 사람이 한도를 늘리면 자르지 않음 · 1.3s · 자동 답변 · 1 call · $0.001196
→ request POST /webhook/my-cost
{
"question": "아래는 프린터 로그를 그대로 붙인 것입니다.\n2026-10-29 09:10:05 spooler[2231]: job 4100 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:10:35 spooler[2231]: job 4101 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:11:05 spooler[2231]: job 4102 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:11:35 spooler[2231]: job 4103 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:12:05 spooler[2231]: job 4104 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:12:35 spooler[2231]: job 4105 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:13:05 spooler[2231]: job 4106 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:13:35 spooler[2231]: job 4107 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:14:05 spooler[2231]: job 4108 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:14:35 spooler[2231]: job 4109 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:15:05 spooler[2231]: job 4110 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:15:35 spooler[2231]: job 4111 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:16:05 spooler[2231]: job 4112 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:16:35 spooler[2231]: job 4113 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:17:05 spooler[2231]: job 4114 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:17:35 spooler[2231]: job 4115 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:18:05 spooler[2231]: job 4116 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:18:35 spooler[2231]: job 4117 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:19:05 spooler[2231]: job 4118 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:19:35 spooler[2231]: job 4119 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:20:05 spooler[2231]: job 4120 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:20:35 spooler[2231]: job 4121 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:21:05 spooler[2231]: job 4122 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:21:35 spooler[2231]: job 4123 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:22:05 spooler[2231]: job 4124 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:22:35 spooler[2231]: job 4125 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:23:05 spooler[2231]: job 4126 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:23:35 spooler[2231]: job 4127 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:24:05 spooler[2231]: job 4128 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:24:35 spooler[2231]: job 4129 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:25:05 spooler[2231]: job 4130 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:25:35 spooler[2231]: job 4131 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:26:05 spooler[2231]: job 4132 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:26:35 spooler[2231]: job 4133 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:27:05 spooler[2231]: job 4134 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:27:35 spooler[2231]: job 4135 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:28:05 spooler[2231]: job 4136 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:28:35 spooler[2231]: job 4137 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:29:05 spooler[2231]: job 4138 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:29:35 spooler[2231]: job 4139 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:30:05 spooler[2231]: job 4140 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:30:35 spooler[2231]: job 4141 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:31:05 spooler[2231]: job 4142 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=2\n2026-10-29 09:31:35 spooler[2231]: job 4143 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=3\n2026-10-29 09:32:05 spooler[2231]: job 4144 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=0\n2026-10-29 09:32:35 spooler[2231]: job 4145 state=held queue=FL3-PRN-01 reason=paused-by-driver retry=1\n2026-10-29 09:33:05 spooler[2231]: job 4146 state=held queue=FL3-PRN-01 reason=paused-Line truncated
"user": "최유진"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 먼저 확인하시고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 티켓은 없으며, 추가 도움이 필요하시면 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 2766,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 2766,
"completion_tokens": 56,
"cost_usd": 0.001196
}
}
},
"cost_usd": 0.001196,
"trimmed": false,
"question_chars": 6298,
"recorded": true
}
Passed 4/4
report: runs/lab02-challenge2-20261028-210653.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0058896, "remaining_usd": 0.994110
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0058896,
"remaining_usd": 0.9941104,
"over": false
}
$ uv run tools/flow.py test cases/lab02-challenge.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ faq-vpn · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000156
✔ faq-password · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000138
✔ faq-printer · LLM 한 번 · $0.0002 이하 · 1.7s · 자동 답변 · 1 call · $0.000152
✔ faq-mail · LLM 한 번 · $0.0002 이하 · 1.4s · 자동 답변 · 1 call · $0.000166
✔ ticket-own · LLM 한 번 · $0.0002 이하 · 1.2s · 자동 답변 · 1 call · $0.000115
✔ ticket-done · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000108
✔ 규칙으로 모르는 질문은 분류 + 답 · 2.0s · 자동 답변 · 2 calls · $0.000097
Passed 7/7
report: runs/lab02-challenge-20261028-210703.json
+124
View File
@@ -0,0 +1,124 @@
{
"cases": "lab03.json",
"webhook": "my-cost",
"passed": 1,
"total": 3,
"results": [
{
"case": "한도를 넘으면 · FAQ 원문",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"route = 자동 답변 (expected 한도 초과)",
"cost_usd = 0.000154 (expected 0)",
"llm_calls = 1 (expected 0)"
],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 55,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 55,
"cost_usd": 0.000154
}
}
},
"cost_usd": 0.000154,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "한도를 넘으면 · 접수 원문",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"route = 자동 답변 (expected 한도 초과)",
"cost_usd = 0.0001152 (expected 0)"
],
"seconds": 1.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 152,
"completion_tokens": 34,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
}
}
},
"cost_usd": 0.0001152,
"trimmed": false,
"question_chars": 19,
"recorded": true
}
},
{
"case": "한도 안이면 평소대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.4,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
}
]
}
+171
View File
@@ -0,0 +1,171 @@
$ uv run tools/flow.py budget
budget $1.0 / used $0.006822 · remaining $0.993178 · over False · per user 20/min · question 500 chars
$ uv run tools/flow.py test cases/lab03.json -v
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 19}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 19
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 0.001, "over":
→ request POST http://localhost:8800/budget
{
"daily_usd": 0.001
}
← response HTTP 200
{
"daily_usd": 0.001,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 0.001,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 1, "total": 1}
→ request POST http://localhost:8800/usage
{
"request_id": "seed",
"user": "seed",
"step": "answer",
"model": "gpt-4.1-mini",
"cost_usd": 0.002
}
← response HTTP 200
{
"stored": 1,
"total": 1
}
✘ 한도를 넘으면 · FAQ 원문 · 1.1s · 자동 답변 · 1 call · $0.000154 · route = 자동 답변 (expected 한도 초과); cost_usd = 0.000154 (expected 0); llm_calls = 1 (expected 0)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓이 없어 추가 지원이 필요하면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 55,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 55,
"cost_usd": 0.000154
}
}
},
"cost_usd": 0.000154,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
✘ 한도를 넘으면 · 접수 원문 · 1.2s · 자동 답변 · 1 call · $0.000115 · route = 자동 답변 (expected 한도 초과); cost_usd = 0.0001152 (expected 0)
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "T-1002 티켓은 현재 처리 중이며, 인증서 발급 승인 대기 중입니다. 10월 20일에 완료될 예정입니다.",
"sources": [],
"llm_calls": 1,
"usage": {
"prompt_tokens": 152,
"completion_tokens": 34,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 152,
"completion_tokens": 34,
"cost_usd": 0.0001152
}
}
},
"cost_usd": 0.0001152,
"trimmed": false,
"question_chars": 19,
"recorded": true
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0022692, "remaining_usd": 0.997730
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0022692,
"remaining_usd": 0.9977308,
"over": false
}
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 3}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 3
}
✔ 한도 안이면 평소대로 · 1.4s · 자동 답변 · 1 call · $0.000141
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
Passed 1/3
report: runs/lab03-20261028-210707.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0001412, "remaining_usd": 0.999858
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0001412,
"remaining_usd": 0.9998588,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 1
}
+97
View File
@@ -0,0 +1,97 @@
{
"cases": "lab03.json",
"webhook": "my-cost",
"passed": 3,
"total": 3,
"results": [
{
"case": "한도를 넘으면 · FAQ 원문",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "한도 초과",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (오늘 자동 답변 한도를 넘어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
},
{
"case": "한도를 넘으면 · 접수 원문",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.2,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "한도 초과",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다. (오늘 자동 답변 한도를 넘어 원문으로 안내합니다.)",
"sources": [],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
},
{
"case": "한도 안이면 평소대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
}
]
}
+153
View File
@@ -0,0 +1,153 @@
$ uv run tools/flow.py push reference/lab03-example1-budget.json
! unpublished '75 IT helpdesk + long question cap (answer)' (w3d4ba9195f92a57): it used the same webhook path
✔ pushed '75 IT helpdesk + daily budget (reference)' (id wd38606e31f611a9)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab03.json -v
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 0.001, "over":
→ request POST http://localhost:8800/budget
{
"daily_usd": 0.001
}
← response HTTP 200
{
"daily_usd": 0.001,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 0.001,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 1, "total": 1}
→ request POST http://localhost:8800/usage
{
"request_id": "seed",
"user": "seed",
"step": "answer",
"model": "gpt-4.1-mini",
"cost_usd": 0.002
}
← response HTTP 200
{
"stored": 1,
"total": 1
}
✔ 한도를 넘으면 · FAQ 원문 · 1.0s · 한도 초과 · 0 calls · $0.000000
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "한도 초과",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (오늘 자동 답변 한도를 넘어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
✔ 한도를 넘으면 · 접수 원문 · 0.2s · 한도 초과 · 0 calls · $0.000000
→ request POST /webhook/my-cost
{
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
}
← response HTTP 200
{
"route": "한도 초과",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다. (오늘 자동 답변 한도를 넘어 원문으로 안내합니다.)",
"sources": [],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.002, "remaining_usd": 0.998, "over
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.002,
"remaining_usd": 0.998,
"over": false
}
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 1
}
✔ 한도 안이면 평소대로 · 1.6s · 자동 답변 · 1 call · $0.000156
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
Passed 3/3
report: runs/lab03-20261028-210722.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0001556, "remaining_usd": 0.999844
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0001556,
"remaining_usd": 0.9998444,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 1}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 1
}
@@ -0,0 +1,161 @@
{
"cases": "lab03-limit.json",
"webhook": "my-cost",
"passed": 3,
"total": 4,
"results": [
{
"case": "이준호 1번째",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "이준호 2번째",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 54,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
}
}
},
"cost_usd": 0.000152,
"trimmed": false,
"question_chars": 12,
"recorded": true
}
},
{
"case": "이준호 3번째 · 1분에 2번 넘음",
"webhook": "my-cost",
"run": 1,
"passed": false,
"problems": [
"took 1.3s (expected at most 1s)",
"status 200 (expected 429)",
"route = 자동 답변 (expected 잠시 후)",
"cost_usd = 0.0001556 (expected 0)"
],
"seconds": 1.3,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "다른 사람은 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.2,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 43,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
}
}
},
"cost_usd": 0.0001364,
"trimmed": false,
"question_chars": 27,
"recorded": true
}
}
]
}
@@ -0,0 +1,180 @@
$ uv run tools/flow.py test cases/lab03-limit.json -v
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 2, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fals
→ request POST http://localhost:8800/budget
{
"user_limit_per_min": 2
}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 2,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 이준호 1번째 · 1.3s · 자동 답변 · 1 call · $0.000156
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
✔ 이준호 2번째 · 1.6s · 자동 답변 · 1 call · $0.000152
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 54,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
}
}
},
"cost_usd": 0.000152,
"trimmed": false,
"question_chars": 12,
"recorded": true
}
✘ 이준호 3번째 · 1분에 2번 넘음 · 1.3s · 자동 답변 · 1 call · $0.000156 · took 1.3s (expected at most 1s); status 200 (expected 429); route = 자동 답변 (expected 잠시 후); cost_usd = 0.0001556 (expected 0)
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 현재 접수된 관련 티켓은 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 56,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 56,
"cost_usd": 0.0001556
}
}
},
"cost_usd": 0.0001556,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
✔ 다른 사람은 그대로 · 1.2s · 자동 답변 · 1 call · $0.000136
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 43,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
}
}
},
"cost_usd": 0.0001364,
"trimmed": false,
"question_chars": 27,
"recorded": true
}
Passed 3/4
report: runs/lab03-limit-20261028-210727.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0005996, "remaining_usd": 0.999400
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0005996,
"remaining_usd": 0.9994004,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 4
}
+139
View File
@@ -0,0 +1,139 @@
{
"cases": "lab03-limit.json",
"webhook": "my-cost",
"passed": 4,
"total": 4,
"results": [
{
"case": "이준호 1번째",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 2.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "이준호 2번째",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.6,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 53,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
}
}
},
"cost_usd": 0.0001504,
"trimmed": false,
"question_chars": 12,
"recorded": true
}
},
{
"case": "이준호 3번째 · 1분에 2번 넘음",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 429,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "잠시 후",
"answer": "요청이 많아 잠시 뒤에 다시 물어봐 주세요.",
"retry_after": 60,
"llm_calls": 0,
"cost_usd": 0
}
},
{
"case": "다른 사람은 그대로",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.9,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 43,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
}
}
},
"cost_usd": 0.0001364,
"trimmed": false,
"question_chars": 27,
"recorded": true
}
}
]
}
+195
View File
@@ -0,0 +1,195 @@
$ uv run tools/flow.py push reference/lab03-example2-limit.json
! unpublished '75 IT helpdesk + safe record (answer)' (w2ae10aa66addee4): it used the same webhook path
✔ pushed '75 IT helpdesk + user limit (reference)' (id w3b18d2741bd22c6)
POST http://localhost:5678/webhook/my-cost
$ uv run tools/flow.py test cases/lab03-limit.json -v
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0029704, "remaining_usd": 0.997029
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.0029704,
"remaining_usd": 0.9970296,
"over": false
}
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 9}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 9
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 2, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fals
→ request POST http://localhost:8800/budget
{
"user_limit_per_min": 2
}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 2,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
POST http://localhost:5678/webhook/my-cost · 4 case(s) × 1
✔ 이준호 1번째 · 2.1s · 자동 답변 · 1 call · $0.000141
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
✔ 이준호 2번째 · 1.6s · 자동 답변 · 1 call · $0.000150
→ request POST /webhook/my-cost
{
"question": "프린터 출력이 안 돼요",
"user": "이준호"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당자가 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 53,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 53,
"cost_usd": 0.0001504
}
}
},
"cost_usd": 0.0001504,
"trimmed": false,
"question_chars": 12,
"recorded": true
}
✔ 이준호 3번째 · 1분에 2번 넘음 · 0.0s · 잠시 후 · 0 calls · $0.000000
→ request POST /webhook/my-cost
{
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
}
← response HTTP 429
{
"route": "잠시 후",
"answer": "요청이 많아 잠시 뒤에 다시 물어봐 주세요.",
"retry_after": 60,
"llm_calls": 0,
"cost_usd": 0
}
✔ 다른 사람은 그대로 · 1.9s · 자동 답변 · 1 call · $0.000136
→ request POST /webhook/my-cost
{
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
}
← response HTTP 200
{
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 43,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
}
}
},
"cost_usd": 0.0001364,
"trimmed": false,
"question_chars": 27,
"recorded": true
}
Passed 4/4
report: runs/lab03-limit-20261028-211138.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.000428, "remaining_usd": 0.999572,
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.000428,
"remaining_usd": 0.999572,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 3}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 3
}
$ uv run tools/flow.py test cases/lab03.json
POST http://localhost:5678/webhook/my-cost · 3 case(s) × 1
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 0.001, "over":
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 1, "total": 1}
✔ 한도를 넘으면 · FAQ 원문 · 0.1s · 한도 초과 · 0 calls · $0.000000
✔ 한도를 넘으면 · 접수 원문 · 0.1s · 한도 초과 · 0 calls · $0.000000
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.002, "remaining_usd": 0.998, "over
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 1}
✔ 한도 안이면 평소대로 · 1.2s · 자동 답변 · 1 call · $0.000156
Passed 3/3
report: runs/lab03-20261028-211139.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.0001556, "remaining_usd": 0.999844
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 1}
$ uv run tools/flow.py test cases/lab02-challenge.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ faq-vpn · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000141
✔ faq-password · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000138
✔ faq-printer · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000152
✔ faq-mail · LLM 한 번 · $0.0002 이하 · 1.5s · 자동 답변 · 1 call · $0.000166
✔ ticket-own · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000115
✔ ticket-done · LLM 한 번 · $0.0002 이하 · 1.1s · 자동 답변 · 1 call · $0.000108
✔ 규칙으로 모르는 질문은 분류 + 답 · 1.7s · 자동 답변 · 2 calls · $0.000097
Passed 7/7
report: runs/lab02-challenge-20261028-211149.json
+198
View File
@@ -0,0 +1,198 @@
{
"cases": "lab03-drill.json",
"webhook": "my-cost",
"passed": 6,
"total": 6,
"results": [
{
"case": "1번째 · VPN",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.1,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "자동 답변",
"answer": "VPN 접속 오류는 VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속하시기 바랍니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-003"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 165,
"completion_tokens": 47,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 165,
"completion_tokens": 47,
"cost_usd": 0.0001412
}
}
},
"cost_usd": 0.0001412,
"trimmed": false,
"question_chars": 24,
"recorded": true
}
},
{
"case": "2번째 · 프린터",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.0,
"status": 200,
"request": {
"question": "프린터 출력이 안 돼요",
"user": "최유진"
},
"response": {
"route": "자동 답변",
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력해 보시기 바랍니다. 현재 접수된 관련 문의는 없습니다. 추가 도움이 필요하시면 담당 직원이 도와드리겠습니다.",
"sources": [
"FAQ-004"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 164,
"completion_tokens": 54,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 164,
"completion_tokens": 54,
"cost_usd": 0.000152
}
}
},
"cost_usd": 0.000152,
"trimmed": false,
"question_chars": 12,
"recorded": true
}
},
{
"case": "3번째 · 비밀번호",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 1.3,
"status": 200,
"request": {
"question": "비밀번호를 잊어버렸어요. 재설정은 어떻게 하나요?",
"user": "한지훈"
},
"response": {
"route": "자동 답변",
"answer": "비밀번호 재설정은 사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정하시면 됩니다. 추가 도움이 필요하시면 문의해 주세요.",
"sources": [
"FAQ-001"
],
"llm_calls": 1,
"usage": {
"prompt_tokens": 169,
"completion_tokens": 43,
"by_step": {
"answer": {
"model": "gpt-4.1-mini",
"prompt_tokens": 169,
"completion_tokens": 43,
"cost_usd": 0.0001364
}
}
},
"cost_usd": 0.0001364,
"trimmed": false,
"question_chars": 27,
"recorded": true
}
},
{
"case": "4번째 · 메일",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "메일함 용량이 꽉 찼대요",
"user": "김민지"
},
"response": {
"route": "한도 초과",
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다. (오늘 자동 답변 한도를 넘어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-005"
],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
},
{
"case": "5번째 · 접수",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"question": "T-1002 처리 상태 알려 주세요",
"user": "박서연"
},
"response": {
"route": "한도 초과",
"answer": "T-1002 접수 건은 현재 '처리 중' 상태입니다. 인증서 발급 승인 대기 중이며 10월 20일 완료 예정입니다. (오늘 자동 답변 한도를 넘어 원문으로 안내합니다.)",
"sources": [],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
},
{
"case": "6번째 · VPN 다시",
"webhook": "my-cost",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"question": "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?",
"user": "이준호"
},
"response": {
"route": "한도 초과",
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다. (오늘 자동 답변 한도를 넘어 FAQ 원문으로 안내합니다.)",
"sources": [
"FAQ-003"
],
"llm_calls": 0,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"by_step": {}
},
"cost_usd": 0
}
}
]
}
+43
View File
@@ -0,0 +1,43 @@
$ uv run tools/flow.py budget --daily 0.0003
✔ budget changed: {"daily_usd": 0.0003}
budget $0.0003 / used $0.000934 · remaining $-0.000634 · over True · per user 20/min · question 500 chars
$ uv run tools/flow.py test cases/lab03-drill.json
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 8}
POST http://localhost:5678/webhook/my-cost · 6 case(s) × 1
✔ 1번째 · VPN · 1.1s · 자동 답변 · 1 call · $0.000141
✔ 2번째 · 프린터 · 1.0s · 자동 답변 · 1 call · $0.000152
✔ 3번째 · 비밀번호 · 1.3s · 자동 답변 · 1 call · $0.000136
✔ 4번째 · 메일 · 0.1s · 한도 초과 · 0 calls · $0.000000
✔ 5번째 · 접수 · 0.1s · 한도 초과 · 0 calls · $0.000000
✔ 6번째 · VPN 다시 · 0.0s · 한도 초과 · 0 calls · $0.000000
Passed 6/6
report: runs/lab03-drill-20261028-210759.json
$ uv run tools/flow.py usage
requests 3 · calls 3 · tokens in 498 / out 144 · $0.000430
model gpt-4.1-mini 3 calls · in 498 · out 144 · $0.000430
user 최유진 1 calls · in 164 · out 54 · $0.000152
user 이준호 1 calls · in 165 · out 47 · $0.000141
user 한지훈 1 calls · in 169 · out 43 · $0.000136
route 자동 답변 3 calls · in 498 · out 144 · $0.000430
budget $0.0003 / used $0.000430 · remaining $-0.000130 · over True · per user 20/min · question 500 chars
$ uv run tools/flow.py budget --reset
✔ budget back to data/budget.json
budget $1.0 / used $0.000430 · remaining $0.999570 · over False · per user 20/min · question 500 chars
$ uv run tools/flow.py test cases/lab03-drill.json
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 3}
POST http://localhost:5678/webhook/my-cost · 6 case(s) × 1
✔ 1번째 · VPN · 1.4s · 자동 답변 · 1 call · $0.000141
✔ 2번째 · 프린터 · 1.2s · 자동 답변 · 1 call · $0.000152
✔ 3번째 · 비밀번호 · 1.6s · 자동 답변 · 1 call · $0.000136
✔ 4번째 · 메일 · 1.8s · 자동 답변 · 1 call · $0.000166
✔ 5번째 · 접수 · 1.4s · 자동 답변 · 1 call · $0.000133
✔ 6번째 · VPN 다시 · 1.5s · 자동 답변 · 1 call · $0.000156
Passed 6/6
report: runs/lab03-drill-20261028-210808.json
$ uv run tools/flow.py usage --clear
✔ usage cleared: {"cleared": 6}
@@ -0,0 +1,111 @@
{
"cases": "lab03-challenge.json",
"webhook": "my-cost-report",
"passed": 0,
"total": 5,
"results": [
{
"case": "요약 숫자",
"webhook": "my-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 404 (expected 200)",
"requests = null (expected 3)",
"calls = null (expected 4)",
"cost_usd = null (expected 0.001989)",
"avg_usd = null (expected 0.000663)",
"top_user = null (expected 박서연)"
],
"seconds": 0.0,
"status": 404,
"request": {
"requests_per_day": 100,
"workdays": 22
},
"response": {
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 404 (expected 200)",
"projected_month_usd = null (expected 1.4586)",
"by_model has none of ['\"gpt-4.1-mini\": 0.001944']"
],
"seconds": 0.0,
"status": 404,
"request": {
"requests_per_day": 100,
"workdays": 22
},
"response": {
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 404 (expected 200)",
"by_model has none of ['\"gpt-5.4-nano\": 4.5e-05']"
],
"seconds": 0.0,
"status": 404,
"request": {},
"response": {
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 404 (expected 200)",
"daily_budget_usd = null (expected 1.0)",
"remaining_usd = null (expected 0.998011)",
"over_budget = null (expected false)"
],
"seconds": 0.0,
"status": 404,
"request": {},
"response": {
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 404 (expected 200)",
"over_budget = null (expected true)"
],
"seconds": 0.0,
"status": 404,
"request": {},
"response": {
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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,162 @@
$ uv run tools/flow.py off my-cost-report
✔ unpublished my-cost-report (id wc7211a27bb523f0)
✔ n8n restarted. Turn it back on with: uv run tools/flow.py push <the workflow file>
$ uv run tools/flow.py test cases/lab03-challenge.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 8}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 8
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 4, "total": 4}
→ request POST http://localhost:8800/usage
{
"items": [
{
"request_id": "r1",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 1000,
"completion_tokens": 100,
"cost_usd": 0.00056,
"route": "자동 답변"
},
{
"request_id": "r1",
"user": "이준호",
"step": "classify",
"model": "gpt-5.4-nano",
"prompt_tokens": 100,
"completion_tokens": 20,
"cost_usd": 4.5e-05,
"route": "자동 답변"
},
{
"request_id": "r2",
"user": "박서연",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 2000,
"completion_tokens": 200,
"cost_usd": 0.00112,
"route": "자동 답변"
},
{
"request_id": "r3",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 500,
"completion_tokens": 40,
"cost_usd": 0.000264,
"route": "자동 답변"
}
]
}
← response HTTP 200
{
"stored": 4,
"total": 4
}
POST http://localhost:5678/webhook/my-cost-report · 5 case(s) × 1
✘ 요약 숫자 · 0.0s · status 404 (expected 200); requests = null (expected 3); calls = null (expected 4); cost_usd = null (expected 0.001989); avg_usd = null (expected 0.000663); top_user = null (expected 박서연)
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22
}
← response HTTP 404
{
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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); projected_month_usd = null (expected 1.4586); by_model has none of ['"gpt-4.1-mini": 0.001944']
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22
}
← response HTTP 404
{
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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); by_model has none of ['"gpt-5.4-nano": 4.5e-05']
→ request POST /webhook/my-cost-report
{}
← response HTTP 404
{
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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); daily_budget_usd = null (expected 1.0); remaining_usd = null (expected 0.998011); over_budget = null (expected false)
→ request POST /webhook/my-cost-report
{}
← response HTTP 404
{
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": -0.0009
→ request POST http://localhost:8800/budget
{
"daily_usd": 0.001
}
← response HTTP 200
{
"daily_usd": 0.001,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": -0.000989,
"over": true
}
✘ 예산을 넘으면 · 0.0s · status 404 (expected 200); over_budget = null (expected true)
→ request POST /webhook/my-cost-report
{}
← response HTTP 404
{
"code": 404,
"message": "The requested webhook \"POST my-cost-report\" 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/5
report: runs/lab03-challenge-20261028-211157.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": 0.998011,
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": 0.998011,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 4
}
+139
View File
@@ -0,0 +1,139 @@
{
"cases": "lab03-challenge.json",
"webhook": "my-cost-report",
"passed": 5,
"total": 5,
"results": [
{
"case": "요약 숫자",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.8,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "모델별 · 한 달 예상",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "작은 모델 몫",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "예산 남은 금액",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "예산을 넘으면",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 0.001,
"remaining_usd": -0.000989,
"over_budget": true
}
}
]
}
+242
View File
@@ -0,0 +1,242 @@
$ uv run tools/flow.py push answers/75-lab03-challenge-report.json
✔ pushed '75 Cost report (answer)' (id w1f9c668c0305daf)
POST http://localhost:5678/webhook/my-cost-report
$ uv run tools/flow.py test cases/lab03-challenge.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 4, "total": 4}
→ request POST http://localhost:8800/usage
{
"items": [
{
"request_id": "r1",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 1000,
"completion_tokens": 100,
"cost_usd": 0.00056,
"route": "자동 답변"
},
{
"request_id": "r1",
"user": "이준호",
"step": "classify",
"model": "gpt-5.4-nano",
"prompt_tokens": 100,
"completion_tokens": 20,
"cost_usd": 4.5e-05,
"route": "자동 답변"
},
{
"request_id": "r2",
"user": "박서연",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 2000,
"completion_tokens": 200,
"cost_usd": 0.00112,
"route": "자동 답변"
},
{
"request_id": "r3",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 500,
"completion_tokens": 40,
"cost_usd": 0.000264,
"route": "자동 답변"
}
]
}
← response HTTP 200
{
"stored": 4,
"total": 4
}
POST http://localhost:5678/webhook/my-cost-report · 5 case(s) × 1
✔ 요약 숫자 · 0.8s · $0.001989
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ 모델별 · 한 달 예상 · 0.0s · $0.001989
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ 작은 모델 몫 · 0.1s · $0.001989
→ request POST /webhook/my-cost-report
{}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ 예산 남은 금액 · 0.0s · $0.001989
→ request POST /webhook/my-cost-report
{}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": -0.0009
→ request POST http://localhost:8800/budget
{
"daily_usd": 0.001
}
← response HTTP 200
{
"daily_usd": 0.001,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": -0.000989,
"over": true
}
✔ 예산을 넘으면 · 0.1s · $0.001989
→ request POST /webhook/my-cost-report
{}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 0.001,
"remaining_usd": -0.000989,
"over_budget": true
}
Passed 5/5
report: runs/lab03-challenge-20261028-211208.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": 0.998011,
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": 0.998011,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 4
}
$ uv run tools/flow.py test cases/lab02-challenge.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ faq-vpn · LLM 한 번 · $0.0002 이하 · 1.3s · 자동 답변 · 1 call · $0.000141
✔ faq-password · LLM 한 번 · $0.0002 이하 · 1.5s · 자동 답변 · 1 call · $0.000136
✔ faq-printer · LLM 한 번 · $0.0002 이하 · 1.4s · 자동 답변 · 1 call · $0.000150
✔ faq-mail · LLM 한 번 · $0.0002 이하 · 1.7s · 자동 답변 · 1 call · $0.000140
✔ ticket-own · LLM 한 번 · $0.0002 이하 · 1.2s · 자동 답변 · 1 call · $0.000115
✔ ticket-done · LLM 한 번 · $0.0002 이하 · 1.0s · 자동 답변 · 1 call · $0.000108
✔ 규칙으로 모르는 질문은 분류 + 답 · 1.7s · 자동 답변 · 2 calls · $0.000097
Passed 7/7
report: runs/lab02-challenge-20261028-211218.json
$ uv run tools/flow.py call my-cost-report '{"requests_per_day": 1000}'
HTTP 200 · 0.1s
{
"requests": 7,
"calls": 8,
"cost_usd": 0.000889,
"avg_usd": 0.000127,
"by_model": {
"gpt-4.1-mini": 0.000864,
"gpt-5.4-nano": 2.5e-05
},
"top_user": "이준호",
"projected_month_usd": 2.795257,
"daily_budget_usd": 1,
"remaining_usd": 0.999111,
"over_budget": false
}
@@ -0,0 +1,157 @@
{
"cases": "lab03-challenge2.json",
"webhook": "my-cost-report",
"passed": 2,
"total": 5,
"results": [
{
"case": "모두 nano였다면",
"webhook": "my-cost-report",
"run": 1,
"passed": false,
"problems": [
"what_if.model = null (expected gpt-5.4-nano)",
"what_if.cost_usd = null (expected 0.00117)",
"what_if.saving_usd = null (expected 0.000819)",
"what_if.projected_month_usd = null (expected 0.858)"
],
"seconds": 0.0,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "원래 숫자는 그대로",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "모두 mini였다면 · 아끼는 돈이 음수",
"webhook": "my-cost-report",
"run": 1,
"passed": false,
"problems": [
"what_if.cost_usd = null (expected 0.002016)",
"what_if.saving_usd = null (expected -2.7e-05)"
],
"seconds": 0.0,
"status": 200,
"request": {
"what_if_model": "gpt-4.1-mini"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "없는 모델은 400",
"webhook": "my-cost-report",
"run": 1,
"passed": false,
"problems": [
"status 200 (expected 400)",
"error = null (expected unknown model)",
"known has none of ['gpt-5.4-nano']"
],
"seconds": 0.0,
"status": 200,
"request": {
"what_if_model": "gpt-9"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
},
{
"case": "what_if 없이 부르면 과제 1과 같음",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
}
]
}
@@ -0,0 +1,200 @@
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 8}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 8
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 4, "total": 4}
→ request POST http://localhost:8800/usage
{
"items": [
{
"request_id": "r1",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 1000,
"completion_tokens": 100,
"cost_usd": 0.00056,
"route": "자동 답변"
},
{
"request_id": "r1",
"user": "이준호",
"step": "classify",
"model": "gpt-5.4-nano",
"prompt_tokens": 100,
"completion_tokens": 20,
"cost_usd": 4.5e-05,
"route": "자동 답변"
},
{
"request_id": "r2",
"user": "박서연",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 2000,
"completion_tokens": 200,
"cost_usd": 0.00112,
"route": "자동 답변"
},
{
"request_id": "r3",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 500,
"completion_tokens": 40,
"cost_usd": 0.000264,
"route": "자동 답변"
}
]
}
← response HTTP 200
{
"stored": 4,
"total": 4
}
POST http://localhost:5678/webhook/my-cost-report · 5 case(s) × 1
✘ 모두 nano였다면 · 0.0s · $0.001989 · what_if.model = null (expected gpt-5.4-nano); what_if.cost_usd = null (expected 0.00117); what_if.saving_usd = null (expected 0.000819); what_if.projected_month_usd = null (expected 0.858)
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ 원래 숫자는 그대로 · 0.1s · $0.001989
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✘ 모두 mini였다면 · 아끼는 돈이 음수 · 0.0s · $0.001989 · what_if.cost_usd = null (expected 0.002016); what_if.saving_usd = null (expected -2.7e-05)
→ request POST /webhook/my-cost-report
{
"what_if_model": "gpt-4.1-mini"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✘ 없는 모델은 400 · 0.0s · $0.001989 · status 200 (expected 400); error = null (expected unknown model); known has none of ['gpt-5.4-nano']
→ request POST /webhook/my-cost-report
{
"what_if_model": "gpt-9"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
✔ what_if 없이 부르면 과제 1과 같음 · 0.0s · $0.001989
→ request POST /webhook/my-cost-report
{}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
Passed 2/5
report: runs/lab03-challenge2-20261028-211219.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": 0.998011,
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": 0.998011,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 4
}
@@ -0,0 +1,156 @@
{
"cases": "lab03-challenge2.json",
"webhook": "my-cost-report",
"passed": 5,
"total": 5,
"results": [
{
"case": "모두 nano였다면",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.9,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-5.4-nano",
"cost_usd": 0.00117,
"saving_usd": 0.000819,
"projected_month_usd": 0.858
}
}
},
{
"case": "원래 숫자는 그대로",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.1,
"status": 200,
"request": {
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-5.4-nano",
"cost_usd": 0.00117,
"saving_usd": 0.000819,
"projected_month_usd": 0.858
}
}
},
{
"case": "모두 mini였다면 · 아끼는 돈이 음수",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {
"what_if_model": "gpt-4.1-mini"
},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-4.1-mini",
"cost_usd": 0.002016,
"saving_usd": -2.7e-05,
"projected_month_usd": 0.044352
}
}
},
{
"case": "없는 모델은 400",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.2,
"status": 400,
"request": {
"what_if_model": "gpt-9"
},
"response": {
"error": "unknown model",
"model": "gpt-9",
"known": [
"gpt-4.1-mini",
"gpt-5.4-nano"
]
}
},
{
"case": "what_if 없이 부르면 과제 1과 같음",
"webhook": "my-cost-report",
"run": 1,
"passed": true,
"problems": [],
"seconds": 0.0,
"status": 200,
"request": {},
"response": {
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
}
]
}
+288
View File
@@ -0,0 +1,288 @@
$ uv run tools/flow.py push answers/75-lab03-challenge2-what-if.json
! unpublished '75 Cost report (answer)' (w1f9c668c0305daf): it used the same webhook path
✔ pushed '75 Cost report + what-if (answer)' (id wc7211a27bb523f0)
POST http://localhost:5678/webhook/my-cost-report
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 0
}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0,
"remaining_usd": 1.0,
"over": false
}
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 4, "total": 4}
→ request POST http://localhost:8800/usage
{
"items": [
{
"request_id": "r1",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 1000,
"completion_tokens": 100,
"cost_usd": 0.00056,
"route": "자동 답변"
},
{
"request_id": "r1",
"user": "이준호",
"step": "classify",
"model": "gpt-5.4-nano",
"prompt_tokens": 100,
"completion_tokens": 20,
"cost_usd": 4.5e-05,
"route": "자동 답변"
},
{
"request_id": "r2",
"user": "박서연",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 2000,
"completion_tokens": 200,
"cost_usd": 0.00112,
"route": "자동 답변"
},
{
"request_id": "r3",
"user": "이준호",
"step": "answer",
"model": "gpt-4.1-mini",
"prompt_tokens": 500,
"completion_tokens": 40,
"cost_usd": 0.000264,
"route": "자동 답변"
}
]
}
← response HTTP 200
{
"stored": 4,
"total": 4
}
POST http://localhost:5678/webhook/my-cost-report · 5 case(s) × 1
✔ 모두 nano였다면 · 0.9s · $0.001989
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-5.4-nano",
"cost_usd": 0.00117,
"saving_usd": 0.000819,
"projected_month_usd": 0.858
}
}
✔ 원래 숫자는 그대로 · 0.1s · $0.001989
→ request POST /webhook/my-cost-report
{
"requests_per_day": 100,
"workdays": 22,
"what_if_model": "gpt-5.4-nano"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 1.4586,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-5.4-nano",
"cost_usd": 0.00117,
"saving_usd": 0.000819,
"projected_month_usd": 0.858
}
}
✔ 모두 mini였다면 · 아끼는 돈이 음수 · 0.0s · $0.001989
→ request POST /webhook/my-cost-report
{
"what_if_model": "gpt-4.1-mini"
}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false,
"what_if": {
"model": "gpt-4.1-mini",
"cost_usd": 0.002016,
"saving_usd": -2.7e-05,
"projected_month_usd": 0.044352
}
}
✔ 없는 모델은 400 · 0.2s
→ request POST /webhook/my-cost-report
{
"what_if_model": "gpt-9"
}
← response HTTP 400
{
"error": "unknown model",
"model": "gpt-9",
"known": [
"gpt-4.1-mini",
"gpt-5.4-nano"
]
}
✔ what_if 없이 부르면 과제 1과 같음 · 0.0s · $0.001989
→ request POST /webhook/my-cost-report
{}
← response HTTP 200
{
"requests": 3,
"calls": 4,
"cost_usd": 0.001989,
"avg_usd": 0.000663,
"by_model": {
"gpt-4.1-mini": 0.001944,
"gpt-5.4-nano": 4.5e-05
},
"top_user": "박서연",
"projected_month_usd": 0.043758,
"daily_budget_usd": 1,
"remaining_usd": 0.998011,
"over_budget": false
}
Passed 5/5
report: runs/lab03-challenge2-20261028-211231.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": 0.998011,
→ request POST http://localhost:8800/budget/reset
{}
← response HTTP 200
{
"daily_usd": 1.0,
"user_limit_per_min": 20,
"max_question_chars": 500,
"used_usd": 0.001989,
"remaining_usd": 0.998011,
"over": false
}
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
→ request POST http://localhost:8800/usage-clear
{}
← response HTTP 200
{
"cleared": 4
}
$ uv run tools/flow.py test cases/lab03-challenge.json
✔ before: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 0}
✔ before: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0, "remaining_usd": 1.0, "over": fal
✔ before: POST http://localhost:8800/usage · HTTP 200 · 0.0s · {"stored": 4, "total": 4}
POST http://localhost:5678/webhook/my-cost-report · 5 case(s) × 1
✔ 요약 숫자 · 0.1s · $0.001989
✔ 모델별 · 한 달 예상 · 0.1s · $0.001989
✔ 작은 모델 몫 · 0.1s · $0.001989
✔ 예산 남은 금액 · 0.0s · $0.001989
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 0.001, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": -0.0009
✔ 예산을 넘으면 · 0.0s · $0.001989
Passed 5/5
report: runs/lab03-challenge-20261028-211232.json
✔ after: POST http://localhost:8800/budget/reset · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.001989, "remaining_usd": 0.998011,
✔ after: POST http://localhost:8800/usage-clear · HTTP 200 · 0.0s · {"cleared": 4}
$ uv run tools/flow.py test cases/lab02-challenge.json
POST http://localhost:5678/webhook/my-cost · 7 case(s) × 1
✔ faq-vpn · LLM 한 번 · $0.0002 이하 · 1.7s · 자동 답변 · 1 call · $0.000156
✔ faq-password · LLM 한 번 · $0.0002 이하 · 1.4s · 자동 답변 · 1 call · $0.000136
✔ faq-printer · LLM 한 번 · $0.0002 이하 · 1.2s · 자동 답변 · 1 call · $0.000150
✔ faq-mail · LLM 한 번 · $0.0002 이하 · 1.4s · 자동 답변 · 1 call · $0.000166
✔ ticket-own · LLM 한 번 · $0.0002 이하 · 1.0s · 자동 답변 · 1 call · $0.000115
✔ ticket-done · LLM 한 번 · $0.0002 이하 · 1.1s · 자동 답변 · 1 call · $0.000108
✔ 규칙으로 모르는 질문은 분류 + 답 · 2.2s · 자동 답변 · 2 calls · $0.000097
Passed 7/7
report: runs/lab02-challenge-20261028-211242.json
$ uv run tools/flow.py call my-cost-report '{"requests_per_day": 1000, "what_if_model": "gpt-5.4-nano"}'
HTTP 200 · 0.1s
{
"requests": 7,
"calls": 8,
"cost_usd": 0.000929,
"avg_usd": 0.000133,
"by_model": {
"gpt-4.1-mini": 0.000904,
"gpt-5.4-nano": 2.5e-05
},
"top_user": "이준호",
"projected_month_usd": 2.920971,
"daily_budget_usd": 1,
"remaining_usd": 0.999071,
"over_budget": false,
"what_if": {
"model": "gpt-5.4-nano",
"cost_usd": 0.00061,
"saving_usd": 0.00032,
"projected_month_usd": 1.9162
}
}
$ uv run tools/flow.py usage --clear
✔ usage cleared: {"cleared": 8}
$ uv run tools/flow.py budget --reset
✔ budget back to data/budget.json
budget $1.0 / used $0.000000 · remaining $1.000000 · over False · per user 20/min · question 500 chars
$ 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
✔ prices /prices · budget /budget (day 75 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,421 @@
{
"name": "75 IT helpdesk + usage record (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "797db9f9-e9ff-5363-9d0a-58d23a2047f0",
"position": [
0,
300
],
"id": "3ccc3834-a583-51bd-a80f-b20124e32b64"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "7e27dce9-fa3b-5c88-98b3-1dce956d16d2"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "f3591be9-5676-5bb7-ab46-6984719af080"
},
{
"name": "Get prices",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/prices",
"options": {}
},
"position": [
1100,
300
],
"id": "baa4796d-564b-5cc0-949a-d79738b12ba7"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1540,
300
],
"id": "e2005518-7a82-579a-acda-cc74b33968a9"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "9d6c9b87-54bc-5433-bad3-6e33744e10ca"
},
{
"name": "Classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You sort one company IT help-desk question. Categories: faq (how to fix or use something), ticket (asks about the status of a ticket like T-1002), other. Return only JSON, no code fence: {\\\"category\\\": \\\"faq\\\"}\" }, { role: 'user', content: $('Question webhook').first().json.body.question }] }) }}",
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2420,
300
],
"id": "07e553a6-f8eb-5513-a566-db99f5b11d29"
},
{
"name": "Answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \\\"찾지 못했습니다\\\" and that a staff member will help. Use 합니다체.\\n\\n참고 자료 (사내 규정 전체):\\n# 업무용 장비 지급·반납 규정\\n\\n## 1. 지급과 교체\\n- 업무용 노트북은 입사일에 지급한다.\\n- 노트북 교체 주기는 4년이다. 고장이나 배터리 교체는 IT 문의로 접수한다.\\n\\n## 2. 분실과 도난\\n- 업무용 장비를 잃어버리거나 도난당하면 24시간 이내에 IT 문의와 보안팀에 신고한다.\\n- 신고가 접수되면 IT 담당자가 원격으로 장비의 데이터를 잠근다.\\n\\n## 3. 반납\\n- 퇴사하거나 장비를 교체할 때는 5영업일 이내에 장비를 IT 담당자에게 반납한다.\\n- 반납 전에 개인 파일을 지우지 말고, 업무 파일은 사내 드라이브로 옮긴다.\\n\\n## 4. 개인 장비 사용\\n- 개인 휴대폰이나 태블릿으로는 업무 메일과 메신저만 쓸 수 있다.\\n- 개인 장비로 업무 메일을 보려면 회사의 모바일 관리 앱(MDM)을 먼저 설치한다.\\n\\n# 메일·문서 보안 규정\\n\\n## 1. 메일함\\n- 개인 메일함 용량은 10GB이다. 용량이 90%를 넘으면 안내 메일이 발송된다.\\n- 오래된 메일은 보관함으로 옮겨 용량을 확보한다.\\n\\n## 2. 첨부 파일\\n- 메일 첨부 파일은 25MB까지 보낼 수 있다.\\n- 25MB를 넘는 파일은 사내 드라이브에 올리고 공유 링크를 보낸다.\\n- 개인정보가 들어 있는 파일을 외부로 보낼 때는 반드시 암호를 걸고, 암호는 메신저 등 다른 경로로 알려 준다.\\n\\n## 3. 문서 등급과 보관\\n- 대외비 문서에는 문서 첫 장에 '대외비'라고 표시한다.\\n- 업무 문서는 3년 동안 보관한 뒤 정리한다.\\n\\n# 회의실 이용 규정\\n\\n## 1. 예약\\n- 회의실은 사내 포털에서 예약한다. 한 번에 최대 3시간까지 예약할 수 있다.\\n- 예약 시작 후 10분이 지나도록 아무도 사용하지 않으면 예약이 자동으로 취소된다.\\n\\n## 2. 장비\\n- 대회의실의 화상회의 장비를 쓰려면 회의 시작 30분 전까지 IT 문의로 요청한다.\\n- 프로젝터는 B회의실과 대회의실에 있다.\\n\\n## 3. 사용 후\\n- 회의가 끝나면 화이트보드를 지우고 의자를 정리한다.\\n\\n# 비밀번호·계정 보안 규정\\n\\n## 1. 비밀번호 만들기\\n- 비밀번호는 12자 이상으로 만든다.\\n- 영문 대문자, 영문 소문자, 숫자, 특수문자 중 3종류 이상을 섞는다.\\n- 이름, 생일, 전화번호, 회사 이름처럼 추측하기 쉬운 단어는 쓰지 않는다.\\n\\n## 2. 변경 주기\\n- 비밀번호는 90일마다 바꾼다. 만료 7일 전부터 사내 포털에 변경 안내가 표시된다.\\n- 최근에 쓴 비밀번호 5개는 다시 쓸 수 없다.\\n\\n## 3. 로그인 실패와 잠금\\n- 로그인에 5번 연속 실패하면 계정이 30분 동안 잠긴다.\\n- 30분이 지나면 자동으로 풀린다. 급한 경우 IT 문의로 잠금 해제를 요청한다.\\n\\n## 4. 다단계 인증\\n- 회사 밖에서 사내 시스템에 접속할 때는 OTP 다단계 인증을 반드시 사용한다.\\n- OTP 앱이 설치된 휴대폰을 바꾸면 IT 포털에서 OTP를 다시 등록한다.\\n\\n# 개인정보 처리 지침\\n\\n## 1. 수집\\n- 업무에 꼭 필요한 개인정보만 수집한다.\\n- 주민등록번호는 법에서 정한 경우가 아니면 수집하지 않는다.\\n\\n## 2. 이용과 보관\\n- 고객 개인정보가 보이는 화면은 캡처하거나 촬영하지 않는다.\\n- 개인정보가 들어 있는 출력물은 다 쓴 뒤 바로 파쇄한다.\\n\\n## 3. 유출 대응\\n- 개인정보가 유출되었거나 유출이 의심되면 1시간 이내에 보안팀에 신고한다.\\n- 신고할 때는 유출된 정보의 종류, 대략적인 건수, 발견한 시각을 함께 알린다.\\n\\n# 원격근무·VPN 이용 규정\\n\\n## 1. 접속 원칙\\n- 회사 밖에서 사내 시스템에 접속할 때는 반드시 회사 VPN을 먼저 연결한다.\\n- 카페, 공항 같은 공용 와이파이에서는 VPN 없이 사내 시스템에 접속하지 않는다.\\n\\n## 2. VPN 인증서\\n- VPN 인증서의 유효 기간은 1년이다.\\n- 만료 30일 전부터 IT 포털의 '인증서 재발급' 메뉴에서 새 인증서를 신청한다.\\n- 인증서가 만료되면 VPN에 접속할 수 없으므로, 만료 전에 재발급한다.\\n\\n## 3. 재택근무 중 보안\\n- 자리를 비울 때는 화면을 잠그고, 5분 동안 사용하지 않으면 자동으로 잠Line truncated
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2640,
300
],
"id": "fd329729-e718-56ff-805c-aa298d26c0b2"
},
{
"name": "Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.\" }, { role: 'user', content: '초안: ' + $('Answer').first().json.choices[0].message.content }] }) }}",
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2860,
300
],
"id": "f289c264-5e1f-523d-8e18-67ba4cbb166c"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst steps = [{\"step\": \"classify\", \"node\": \"Classify\", \"model\": \"gpt-4.1-mini\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}, {\"step\": \"review\", \"node\": \"Review\", \"model\": \"gpt-4.1-mini\"}];\nconst prices = $('Get prices').first().json.models;\nconst by_step = {};\nlet prompt_tokens = 0, completion_tokens = 0, llm_calls = 0, cost_usd = 0;\nfor (const s of steps) {\n if (!ran(s.node)) continue;\n const u = $(s.node).first().json.usage ?? {};\n const row = { model: s.model, prompt_tokens: u.prompt_tokens ?? 0, completion_tokens: u.completion_tokens ?? 0 };\n const p = prices[s.model] ?? { input: 0, output: 0 };\n row.cost_usd = Math.round((row.prompt_tokens * p.input + row.completion_tokens * p.output) / 1e6 * 1e8) / 1e8;\n by_step[s.step] = row;\n prompt_tokens += row.prompt_tokens; completion_tokens += row.completion_tokens; cost_usd += row.cost_usd; llm_calls += 1;\n}\nconst results = $('Search FAQ').first().json.results ?? [];\nconst answer = String($('Review').first().json.choices[0].message.content ?? '').trim();\nconst result = { route: '자동 답변', answer, sources: results.map((r) => r.id), llm_calls,\n usage: { prompt_tokens, completion_tokens, by_step }, cost_usd: Math.round(cost_usd * 1e8) / 1e8 };\nreturn [{ json: result }];"
},
"position": [
3520,
300
],
"id": "d85f4d2b-0957-5a81-a54b-35ba55509871"
},
{
"name": "Record usage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "http://data-api:8800/usage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ items: Object.entries($json.usage.by_step).map(([step, row]) => ({ request_id: $execution.id, user: $('Question webhook').first().json.body.user ?? '', step, model: row.model, prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cost_usd: row.cost_usd, route: $json.route })) }) }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
}
},
"position": [
3740,
300
],
"id": "a46b3503-5039-5cb6-890e-69149fe23f55"
},
{
"name": "Result again",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: $('Build result').first().json }];"
},
"position": [
3960,
300
],
"id": "1b8127ae-a43b-5096-8f94-c96892a085db"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
4180,
300
],
"id": "6cbcbfe3-9306-5287-b0c6-79f3fe0f9a5e"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Get prices",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Get prices": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "Review",
"type": "main",
"index": 0
}
]
]
},
"Review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Record usage",
"type": "main",
"index": 0
}
]
]
},
"Record usage": {
"main": [
[
{
"node": "Result again",
"type": "main",
"index": 0
}
]
]
},
"Result again": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,423 @@
{
"name": "75 IT helpdesk + safe record (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "797db9f9-e9ff-5363-9d0a-58d23a2047f0",
"position": [
0,
300
],
"id": "3ccc3834-a583-51bd-a80f-b20124e32b64"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "7e27dce9-fa3b-5c88-98b3-1dce956d16d2"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "f3591be9-5676-5bb7-ab46-6984719af080"
},
{
"name": "Get prices",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/prices",
"options": {}
},
"position": [
1100,
300
],
"id": "baa4796d-564b-5cc0-949a-d79738b12ba7"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1540,
300
],
"id": "e2005518-7a82-579a-acda-cc74b33968a9"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "9d6c9b87-54bc-5433-bad3-6e33744e10ca"
},
{
"name": "Classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You sort one company IT help-desk question. Categories: faq (how to fix or use something), ticket (asks about the status of a ticket like T-1002), other. Return only JSON, no code fence: {\\\"category\\\": \\\"faq\\\"}\" }, { role: 'user', content: $('Question webhook').first().json.body.question }] }) }}",
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2420,
300
],
"id": "07e553a6-f8eb-5513-a566-db99f5b11d29"
},
{
"name": "Answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \\\"찾지 못했습니다\\\" and that a staff member will help. Use 합니다체.\\n\\n참고 자료 (사내 규정 전체):\\n# 업무용 장비 지급·반납 규정\\n\\n## 1. 지급과 교체\\n- 업무용 노트북은 입사일에 지급한다.\\n- 노트북 교체 주기는 4년이다. 고장이나 배터리 교체는 IT 문의로 접수한다.\\n\\n## 2. 분실과 도난\\n- 업무용 장비를 잃어버리거나 도난당하면 24시간 이내에 IT 문의와 보안팀에 신고한다.\\n- 신고가 접수되면 IT 담당자가 원격으로 장비의 데이터를 잠근다.\\n\\n## 3. 반납\\n- 퇴사하거나 장비를 교체할 때는 5영업일 이내에 장비를 IT 담당자에게 반납한다.\\n- 반납 전에 개인 파일을 지우지 말고, 업무 파일은 사내 드라이브로 옮긴다.\\n\\n## 4. 개인 장비 사용\\n- 개인 휴대폰이나 태블릿으로는 업무 메일과 메신저만 쓸 수 있다.\\n- 개인 장비로 업무 메일을 보려면 회사의 모바일 관리 앱(MDM)을 먼저 설치한다.\\n\\n# 메일·문서 보안 규정\\n\\n## 1. 메일함\\n- 개인 메일함 용량은 10GB이다. 용량이 90%를 넘으면 안내 메일이 발송된다.\\n- 오래된 메일은 보관함으로 옮겨 용량을 확보한다.\\n\\n## 2. 첨부 파일\\n- 메일 첨부 파일은 25MB까지 보낼 수 있다.\\n- 25MB를 넘는 파일은 사내 드라이브에 올리고 공유 링크를 보낸다.\\n- 개인정보가 들어 있는 파일을 외부로 보낼 때는 반드시 암호를 걸고, 암호는 메신저 등 다른 경로로 알려 준다.\\n\\n## 3. 문서 등급과 보관\\n- 대외비 문서에는 문서 첫 장에 '대외비'라고 표시한다.\\n- 업무 문서는 3년 동안 보관한 뒤 정리한다.\\n\\n# 회의실 이용 규정\\n\\n## 1. 예약\\n- 회의실은 사내 포털에서 예약한다. 한 번에 최대 3시간까지 예약할 수 있다.\\n- 예약 시작 후 10분이 지나도록 아무도 사용하지 않으면 예약이 자동으로 취소된다.\\n\\n## 2. 장비\\n- 대회의실의 화상회의 장비를 쓰려면 회의 시작 30분 전까지 IT 문의로 요청한다.\\n- 프로젝터는 B회의실과 대회의실에 있다.\\n\\n## 3. 사용 후\\n- 회의가 끝나면 화이트보드를 지우고 의자를 정리한다.\\n\\n# 비밀번호·계정 보안 규정\\n\\n## 1. 비밀번호 만들기\\n- 비밀번호는 12자 이상으로 만든다.\\n- 영문 대문자, 영문 소문자, 숫자, 특수문자 중 3종류 이상을 섞는다.\\n- 이름, 생일, 전화번호, 회사 이름처럼 추측하기 쉬운 단어는 쓰지 않는다.\\n\\n## 2. 변경 주기\\n- 비밀번호는 90일마다 바꾼다. 만료 7일 전부터 사내 포털에 변경 안내가 표시된다.\\n- 최근에 쓴 비밀번호 5개는 다시 쓸 수 없다.\\n\\n## 3. 로그인 실패와 잠금\\n- 로그인에 5번 연속 실패하면 계정이 30분 동안 잠긴다.\\n- 30분이 지나면 자동으로 풀린다. 급한 경우 IT 문의로 잠금 해제를 요청한다.\\n\\n## 4. 다단계 인증\\n- 회사 밖에서 사내 시스템에 접속할 때는 OTP 다단계 인증을 반드시 사용한다.\\n- OTP 앱이 설치된 휴대폰을 바꾸면 IT 포털에서 OTP를 다시 등록한다.\\n\\n# 개인정보 처리 지침\\n\\n## 1. 수집\\n- 업무에 꼭 필요한 개인정보만 수집한다.\\n- 주민등록번호는 법에서 정한 경우가 아니면 수집하지 않는다.\\n\\n## 2. 이용과 보관\\n- 고객 개인정보가 보이는 화면은 캡처하거나 촬영하지 않는다.\\n- 개인정보가 들어 있는 출력물은 다 쓴 뒤 바로 파쇄한다.\\n\\n## 3. 유출 대응\\n- 개인정보가 유출되었거나 유출이 의심되면 1시간 이내에 보안팀에 신고한다.\\n- 신고할 때는 유출된 정보의 종류, 대략적인 건수, 발견한 시각을 함께 알린다.\\n\\n# 원격근무·VPN 이용 규정\\n\\n## 1. 접속 원칙\\n- 회사 밖에서 사내 시스템에 접속할 때는 반드시 회사 VPN을 먼저 연결한다.\\n- 카페, 공항 같은 공용 와이파이에서는 VPN 없이 사내 시스템에 접속하지 않는다.\\n\\n## 2. VPN 인증서\\n- VPN 인증서의 유효 기간은 1년이다.\\n- 만료 30일 전부터 IT 포털의 '인증서 재발급' 메뉴에서 새 인증서를 신청한다.\\n- 인증서가 만료되면 VPN에 접속할 수 없으므로, 만료 전에 재발급한다.\\n\\n## 3. 재택근무 중 보안\\n- 자리를 비울 때는 화면을 잠그고, 5분 동안 사용하지 않으면 자동으로 잠Line truncated
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2640,
300
],
"id": "fd329729-e718-56ff-805c-aa298d26c0b2"
},
{
"name": "Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, messages: [{ role: 'system', content: \"You review a Korean IT help-desk answer before it is sent. Keep the facts exactly. Make it polite (합니다체), remove anything not supported by the data, keep at most 3 sentences. Return only the final answer text.\" }, { role: 'user', content: '초안: ' + $('Answer').first().json.choices[0].message.content }] }) }}",
"options": {
"timeout": 60000
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"position": [
2860,
300
],
"id": "f289c264-5e1f-523d-8e18-67ba4cbb166c"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst steps = [{\"step\": \"classify\", \"node\": \"Classify\", \"model\": \"gpt-4.1-mini\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}, {\"step\": \"review\", \"node\": \"Review\", \"model\": \"gpt-4.1-mini\"}];\nconst prices = $('Get prices').first().json.models;\nconst by_step = {};\nlet prompt_tokens = 0, completion_tokens = 0, llm_calls = 0, cost_usd = 0;\nfor (const s of steps) {\n if (!ran(s.node)) continue;\n const u = $(s.node).first().json.usage ?? {};\n const row = { model: s.model, prompt_tokens: u.prompt_tokens ?? 0, completion_tokens: u.completion_tokens ?? 0 };\n const p = prices[s.model] ?? { input: 0, output: 0 };\n row.cost_usd = Math.round((row.prompt_tokens * p.input + row.completion_tokens * p.output) / 1e6 * 1e8) / 1e8;\n by_step[s.step] = row;\n prompt_tokens += row.prompt_tokens; completion_tokens += row.completion_tokens; cost_usd += row.cost_usd; llm_calls += 1;\n}\nconst results = $('Search FAQ').first().json.results ?? [];\nconst answer = String($('Review').first().json.choices[0].message.content ?? '').trim();\nconst result = { route: '자동 답변', answer, sources: results.map((r) => r.id), llm_calls,\n usage: { prompt_tokens, completion_tokens, by_step }, cost_usd: Math.round(cost_usd * 1e8) / 1e8 };\nreturn [{ json: result }];"
},
"position": [
3520,
300
],
"id": "d85f4d2b-0957-5a81-a54b-35ba55509871"
},
{
"name": "Record usage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "http://data-api:8800/usage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ items: Object.entries($json.usage.by_step).map(([step, row]) => ({ request_id: $execution.id, user: $('Question webhook').first().json.body.user ?? '', step, model: row.model, prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cost_usd: row.cost_usd, route: $json.route })) }) }}",
"options": {
"response": {
"response": {
"neverError": true
}
},
"timeout": 3000
}
},
"position": [
3740,
300
],
"onError": "continueRegularOutput",
"id": "a46b3503-5039-5cb6-890e-69149fe23f55"
},
{
"name": "Result again",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$('Build result').first().json, recorded: Number($json.stored ?? 0) > 0 } }];"
},
"position": [
3960,
300
],
"id": "1b8127ae-a43b-5096-8f94-c96892a085db"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
4180,
300
],
"id": "6cbcbfe3-9306-5287-b0c6-79f3fe0f9a5e"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Get prices",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Get prices": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "Review",
"type": "main",
"index": 0
}
]
]
},
"Review": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Record usage",
"type": "main",
"index": 0
}
]
]
},
"Record usage": {
"main": [
[
{
"node": "Result again",
"type": "main",
"index": 0
}
]
]
},
"Result again": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,483 @@
{
"name": "75 IT helpdesk + one LLM call (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "797db9f9-e9ff-5363-9d0a-58d23a2047f0",
"position": [
0,
300
],
"id": "3ccc3834-a583-51bd-a80f-b20124e32b64"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "7e27dce9-fa3b-5c88-98b3-1dce956d16d2"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "f3591be9-5676-5bb7-ab46-6984719af080"
},
{
"name": "Get prices",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/prices",
"options": {}
},
"position": [
1100,
300
],
"id": "baa4796d-564b-5cc0-949a-d79738b12ba7"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Question webhook').first().json.body.question }}"
}
]
}
},
"position": [
1540,
300
],
"id": "e2005518-7a82-579a-acda-cc74b33968a9"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "9d6c9b87-54bc-5433-bad3-6e33744e10ca"
},
{
"name": "Rule classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const q = String($('Question webhook').first().json.body.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nconst category = /T-?\\d{4}/i.test(q) ? 'ticket' : (results.length ? 'faq' : 'unknown');\nreturn [{ json: { category } }];"
},
"position": [
2420,
300
],
"id": "ff9310ae-451e-5825-bb13-67013e490b85"
},
{
"name": "Known?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.category }}",
"rightValue": "unknown",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
2640,
300
],
"id": "14570236-0f99-5931-8173-d03f18288d68"
},
{
"name": "Classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-5.4-nano', temperature: 0, messages: [{ role: 'system', content: \"You sort one company IT help-desk question. Categories: faq (how to fix or use something), ticket (asks about the status of a ticket like T-1002), other. Return only JSON, no code fence: {\\\"category\\\": \\\"faq\\\"}\" }, { role: 'user', content: $('Question webhook').first().json.body.question }] }) }}",
"options": {
"timeout": 60000
}
},
"position": [
2860,
520
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"id": "07e553a6-f8eb-5513-a566-db99f5b11d29"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.choices?.[0]?.message?.content ?? '').replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nreturn [{ json: { category } }];"
},
"position": [
3080,
520
],
"id": "1a1ea3bc-20b6-5218-b12e-9bafa39f3d8c"
},
{
"name": "Answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, max_completion_tokens: 200, messages: [{ role: 'system', content: \"You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \\\"찾지 못했습니다\\\" and that a staff member will help. Use 합니다체.\" }, { role: 'user', content: '질문: ' + $('Question webhook').first().json.body.question + '\\nFAQ: ' + JSON.stringify(($('Search FAQ').first().json.results ?? []).map((f) => ({ id: f.id, title: f.title, answer: f.answer }))) + '\\n접수 건: ' + JSON.stringify((({ ticket_id, title, status, note, error }) => ({ ticket_id, title, status, note, error }))($('Get ticket').first().json)) }] }) }}",
"options": {
"timeout": 60000
}
},
"position": [
3300,
300
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"id": "fd329729-e718-56ff-805c-aa298d26c0b2"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst steps = [{\"step\": \"classify\", \"node\": \"Classify\", \"model\": \"gpt-5.4-nano\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}];\nconst prices = $('Get prices').first().json.models;\nconst by_step = {};\nlet prompt_tokens = 0, completion_tokens = 0, llm_calls = 0, cost_usd = 0;\nfor (const s of steps) {\n if (!ran(s.node)) continue;\n const u = $(s.node).first().json.usage ?? {};\n const row = { model: s.model, prompt_tokens: u.prompt_tokens ?? 0, completion_tokens: u.completion_tokens ?? 0 };\n const p = prices[s.model] ?? { input: 0, output: 0 };\n row.cost_usd = Math.round((row.prompt_tokens * p.input + row.completion_tokens * p.output) / 1e6 * 1e8) / 1e8;\n by_step[s.step] = row;\n prompt_tokens += row.prompt_tokens; completion_tokens += row.completion_tokens; cost_usd += row.cost_usd; llm_calls += 1;\n}\nconst results = $('Search FAQ').first().json.results ?? [];\nconst answer = String($('Answer').first().json.choices[0].message.content ?? '').trim();\nconst result = { route: '자동 답변', answer, sources: results.map((r) => r.id), llm_calls,\n usage: { prompt_tokens, completion_tokens, by_step }, cost_usd: Math.round(cost_usd * 1e8) / 1e8 };\nreturn [{ json: result }];"
},
"position": [
3520,
300
],
"id": "d85f4d2b-0957-5a81-a54b-35ba55509871"
},
{
"name": "Record usage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "http://data-api:8800/usage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ items: Object.entries($json.usage.by_step).map(([step, row]) => ({ request_id: $execution.id, user: $('Question webhook').first().json.body.user ?? '', step, model: row.model, prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cost_usd: row.cost_usd, route: $json.route })) }) }}",
"options": {
"response": {
"response": {
"neverError": true
}
},
"timeout": 3000
}
},
"position": [
3740,
300
],
"onError": "continueRegularOutput",
"id": "a46b3503-5039-5cb6-890e-69149fe23f55"
},
{
"name": "Result again",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$('Build result').first().json, recorded: Number($json.stored ?? 0) > 0 } }];"
},
"position": [
3960,
300
],
"id": "1b8127ae-a43b-5096-8f94-c96892a085db"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
4180,
300
],
"id": "6cbcbfe3-9306-5287-b0c6-79f3fe0f9a5e"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Get prices",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Get prices": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "Rule classify",
"type": "main",
"index": 0
}
]
]
},
"Rule classify": {
"main": [
[
{
"node": "Known?",
"type": "main",
"index": 0
}
]
]
},
"Known?": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
],
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Record usage",
"type": "main",
"index": 0
}
]
]
},
"Record usage": {
"main": [
[
{
"node": "Result again",
"type": "main",
"index": 0
}
]
]
},
"Result again": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,532 @@
{
"name": "75 IT helpdesk + long question cap (answer)",
"nodes": [
{
"name": "Question webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "797db9f9-e9ff-5363-9d0a-58d23a2047f0",
"position": [
0,
300
],
"id": "3ccc3834-a583-51bd-a80f-b20124e32b64"
},
{
"name": "Has question?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.body.question }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
220,
300
],
"id": "7e27dce9-fa3b-5c88-98b3-1dce956d16d2"
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"question is required\" }",
"options": {
"responseCode": 400
}
},
"position": [
440,
560
],
"id": "f3591be9-5676-5bb7-ab46-6984719af080"
},
{
"name": "Get budget",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/budget",
"options": {}
},
"position": [
440,
300
],
"id": "4d62a650-0746-5e07-b811-2da4fdd28051"
},
{
"name": "Get prices",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/prices",
"options": {}
},
"position": [
1100,
300
],
"id": "baa4796d-564b-5cc0-949a-d79738b12ba7"
},
{
"name": "Prepare question",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const q = String($('Question webhook').first().json.body.question ?? '');\nconst max = Number($('Get budget').first().json.max_question_chars ?? 500);\nif (q.length <= max) return [{ json: { question: q, trimmed: false, question_chars: q.length } }];\nconst head = Math.floor(max * 0.4);\nconst question = q.slice(0, head) + ' … ' + q.slice(q.length - (max - head));\nreturn [{ json: { question, trimmed: true, question_chars: q.length } }];"
},
"position": [
1320,
300
],
"id": "07d55a70-f59d-584f-bbae-3cfcf59973ac"
},
{
"name": "Search FAQ",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/faq",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $('Prepare question').first().json.question }}"
}
]
}
},
"position": [
1540,
300
],
"id": "e2005518-7a82-579a-acda-cc74b33968a9"
},
{
"name": "Get ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "requester",
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
}
]
}
},
"position": [
1760,
300
],
"id": "9d6c9b87-54bc-5433-bad3-6e33744e10ca"
},
{
"name": "Rule classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const q = String($('Prepare question').first().json.question ?? '');\nconst results = $('Search FAQ').first().json.results ?? [];\nconst category = /T-?\\d{4}/i.test(q) ? 'ticket' : (results.length ? 'faq' : 'unknown');\nreturn [{ json: { category } }];"
},
"position": [
2420,
300
],
"id": "ff9310ae-451e-5825-bb13-67013e490b85"
},
{
"name": "Known?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.category }}",
"rightValue": "unknown",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
2640,
300
],
"id": "14570236-0f99-5931-8173-d03f18288d68"
},
{
"name": "Classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-5.4-nano', temperature: 0, messages: [{ role: 'system', content: \"You sort one company IT help-desk question. Categories: faq (how to fix or use something), ticket (asks about the status of a ticket like T-1002), other. Return only JSON, no code fence: {\\\"category\\\": \\\"faq\\\"}\" }, { role: 'user', content: $('Prepare question').first().json.question }] }) }}",
"options": {
"timeout": 60000
}
},
"position": [
2860,
520
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"id": "07e553a6-f8eb-5513-a566-db99f5b11d29"
},
{
"name": "Parse category",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const raw = String($json.choices?.[0]?.message?.content ?? '').replace(/^```(json)?\\s*|```$/g, '');\nlet category = 'other';\ntry { category = JSON.parse(raw).category ?? 'other'; } catch (error) { category = 'other'; }\nreturn [{ json: { category } }];"
},
"position": [
3080,
520
],
"id": "1a1ea3bc-20b6-5218-b12e-9bafa39f3d8c"
},
{
"name": "Answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0, max_completion_tokens: 200, messages: [{ role: 'system', content: \"You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \\\"찾지 못했습니다\\\" and that a staff member will help. Use 합니다체.\" }, { role: 'user', content: '질문: ' + $('Prepare question').first().json.question + '\\nFAQ: ' + JSON.stringify(($('Search FAQ').first().json.results ?? []).map((f) => ({ id: f.id, title: f.title, answer: f.answer }))) + '\\n접수 건: ' + JSON.stringify((({ ticket_id, title, status, note, error }) => ({ ticket_id, title, status, note, error }))($('Get ticket').first().json)) }] }) }}",
"options": {
"timeout": 60000
}
},
"position": [
3300,
300
],
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
},
"id": "fd329729-e718-56ff-805c-aa298d26c0b2"
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst steps = [{\"step\": \"classify\", \"node\": \"Classify\", \"model\": \"gpt-5.4-nano\"}, {\"step\": \"answer\", \"node\": \"Answer\", \"model\": \"gpt-4.1-mini\"}];\nconst prices = $('Get prices').first().json.models;\nconst by_step = {};\nlet prompt_tokens = 0, completion_tokens = 0, llm_calls = 0, cost_usd = 0;\nfor (const s of steps) {\n if (!ran(s.node)) continue;\n const u = $(s.node).first().json.usage ?? {};\n const row = { model: s.model, prompt_tokens: u.prompt_tokens ?? 0, completion_tokens: u.completion_tokens ?? 0 };\n const p = prices[s.model] ?? { input: 0, output: 0 };\n row.cost_usd = Math.round((row.prompt_tokens * p.input + row.completion_tokens * p.output) / 1e6 * 1e8) / 1e8;\n by_step[s.step] = row;\n prompt_tokens += row.prompt_tokens; completion_tokens += row.completion_tokens; cost_usd += row.cost_usd; llm_calls += 1;\n}\nconst results = $('Search FAQ').first().json.results ?? [];\nconst answer = String($('Answer').first().json.choices[0].message.content ?? '').trim();\nconst result = { route: '자동 답변', answer, sources: results.map((r) => r.id), llm_calls,\n usage: { prompt_tokens, completion_tokens, by_step }, cost_usd: Math.round(cost_usd * 1e8) / 1e8 };\nconst prepared = $('Prepare question').first().json;\nresult.trimmed = prepared.trimmed;\nresult.question_chars = prepared.question_chars;\nreturn [{ json: result }];"
},
"position": [
3520,
300
],
"id": "d85f4d2b-0957-5a81-a54b-35ba55509871"
},
{
"name": "Record usage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"method": "POST",
"url": "http://data-api:8800/usage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ items: Object.entries($json.usage.by_step).map(([step, row]) => ({ request_id: $execution.id, user: $('Question webhook').first().json.body.user ?? '', step, model: row.model, prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cost_usd: row.cost_usd, route: $json.route })) }) }}",
"options": {
"response": {
"response": {
"neverError": true
}
},
"timeout": 3000
}
},
"position": [
3740,
300
],
"onError": "continueRegularOutput",
"id": "a46b3503-5039-5cb6-890e-69149fe23f55"
},
{
"name": "Result again",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "return [{ json: { ...$('Build result').first().json, recorded: Number($json.stored ?? 0) > 0 } }];"
},
"position": [
3960,
300
],
"id": "1b8127ae-a43b-5096-8f94-c96892a085db"
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
4180,
300
],
"id": "6cbcbfe3-9306-5287-b0c6-79f3fe0f9a5e"
}
],
"connections": {
"Question webhook": {
"main": [
[
{
"node": "Has question?",
"type": "main",
"index": 0
}
]
]
},
"Has question?": {
"main": [
[
{
"node": "Get budget",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Get budget": {
"main": [
[
{
"node": "Get prices",
"type": "main",
"index": 0
}
]
]
},
"Get prices": {
"main": [
[
{
"node": "Prepare question",
"type": "main",
"index": 0
}
]
]
},
"Prepare question": {
"main": [
[
{
"node": "Search FAQ",
"type": "main",
"index": 0
}
]
]
},
"Search FAQ": {
"main": [
[
{
"node": "Get ticket",
"type": "main",
"index": 0
}
]
]
},
"Get ticket": {
"main": [
[
{
"node": "Rule classify",
"type": "main",
"index": 0
}
]
]
},
"Rule classify": {
"main": [
[
{
"node": "Known?",
"type": "main",
"index": 0
}
]
]
},
"Known?": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
],
[
{
"node": "Classify",
"type": "main",
"index": 0
}
]
]
},
"Answer": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Record usage",
"type": "main",
"index": 0
}
]
]
},
"Record usage": {
"main": [
[
{
"node": "Result again",
"type": "main",
"index": 0
}
]
]
},
"Result again": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
},
"Classify": {
"main": [
[
{
"node": "Parse category",
"type": "main",
"index": 0
}
]
]
},
"Parse category": {
"main": [
[
{
"node": "Answer",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,127 @@
{
"name": "75 Cost report (answer)",
"nodes": [
{
"name": "Report webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost-report",
"responseMode": "responseNode",
"options": {}
},
"position": [
0,
300
],
"id": "45dddbe3-765d-5071-8c44-f1406634465b",
"webhookId": "49a4308f-5df9-5973-963a-3d7f6bbeb9b7"
},
{
"name": "Get summary",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/usage/summary",
"options": {}
},
"position": [
220,
300
],
"id": "cf6e061b-9073-57ea-a564-1769eec23afd"
},
{
"name": "Get budget",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/budget",
"options": {}
},
"position": [
440,
300
],
"id": "fb9bdc4e-453a-5261-9b27-f913533bebb5"
},
{
"name": "Make report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const s = $('Get summary').first().json;\nconst b = $('Get budget').first().json;\nconst body = $('Report webhook').first().json.body ?? {};\nconst perDay = Number(body.requests_per_day ?? s.requests);\nconst workdays = Number(body.workdays ?? 22);\nconst avg = s.requests ? s.cost_usd / s.requests : 0;\nconst round = (v) => Math.round(v * 1e6) / 1e6;\nconst users = Object.entries(s.by_user).sort((a, b2) => b2[1].cost_usd - a[1].cost_usd);\nconst models = Object.fromEntries(Object.entries(s.by_model).map(([k, v]) => [k, round(v.cost_usd)]));\nconst result = { requests: s.requests, calls: s.calls, cost_usd: round(s.cost_usd), avg_usd: round(avg),\n by_model: models, top_user: users.length ? users[0][0] : null,\n projected_month_usd: round(avg * perDay * workdays), daily_budget_usd: b.daily_usd, remaining_usd: round(b.remaining_usd), over_budget: b.over };\nreturn [{ json: result }];"
},
"position": [
880,
300
],
"id": "ee73c6a8-12a8-57fc-ab34-81fd258667cc"
},
{
"name": "Send report",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
1100,
300
],
"id": "c124f9d7-3b8c-5134-8343-936873621946"
}
],
"connections": {
"Report webhook": {
"main": [
[
{
"node": "Get summary",
"type": "main",
"index": 0
}
]
]
},
"Get summary": {
"main": [
[
{
"node": "Get budget",
"type": "main",
"index": 0
}
]
]
},
"Get budget": {
"main": [
[
{
"node": "Make report",
"type": "main",
"index": 0
}
]
]
},
"Make report": {
"main": [
[
{
"node": "Send report",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,220 @@
{
"name": "75 Cost report + what-if (answer)",
"nodes": [
{
"name": "Report webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"parameters": {
"httpMethod": "POST",
"path": "my-cost-report",
"responseMode": "responseNode",
"options": {}
},
"position": [
0,
300
],
"id": "45dddbe3-765d-5071-8c44-f1406634465b",
"webhookId": "49a4308f-5df9-5973-963a-3d7f6bbeb9b7"
},
{
"name": "Get summary",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/usage/summary",
"options": {}
},
"position": [
220,
300
],
"id": "cf6e061b-9073-57ea-a564-1769eec23afd"
},
{
"name": "Get budget",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/budget",
"options": {}
},
"position": [
440,
300
],
"id": "fb9bdc4e-453a-5261-9b27-f913533bebb5"
},
{
"name": "Get prices",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"parameters": {
"url": "http://data-api:8800/prices",
"options": {}
},
"position": [
660,
300
],
"id": "c0db1fd8-4b10-5824-a526-85ebbd70230a"
},
{
"name": "Make report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const s = $('Get summary').first().json;\nconst b = $('Get budget').first().json;\nconst body = $('Report webhook').first().json.body ?? {};\nconst perDay = Number(body.requests_per_day ?? s.requests);\nconst workdays = Number(body.workdays ?? 22);\nconst avg = s.requests ? s.cost_usd / s.requests : 0;\nconst round = (v) => Math.round(v * 1e6) / 1e6;\nconst users = Object.entries(s.by_user).sort((a, b2) => b2[1].cost_usd - a[1].cost_usd);\nconst models = Object.fromEntries(Object.entries(s.by_model).map(([k, v]) => [k, round(v.cost_usd)]));\nconst result = { requests: s.requests, calls: s.calls, cost_usd: round(s.cost_usd), avg_usd: round(avg),\n by_model: models, top_user: users.length ? users[0][0] : null,\n projected_month_usd: round(avg * perDay * workdays), daily_budget_usd: b.daily_usd, remaining_usd: round(b.remaining_usd), over_budget: b.over };\nconst target = body.what_if_model;\nif (target) {\n const prices = $('Get prices').first().json.models;\n const p = prices[target];\n if (!p) return [{ json: { error: 'unknown model', model: target, known: Object.keys(prices) } }];\n const rows = Object.values(s.by_model);\n const tokensIn = rows.reduce((sum, r) => sum + r.prompt_tokens, 0);\n const tokensOut = rows.reduce((sum, r) => sum + r.completion_tokens, 0);\n const cost = (tokensIn * p.input + tokensOut * p.output) / 1e6;\n result.what_if = { model: target, cost_usd: round(cost), saving_usd: round(s.cost_usd - cost),\n projected_month_usd: round((s.requests ? cost / s.requests : 0) * perDay * workdays) };\n}\nreturn [{ json: result }];"
},
"position": [
880,
300
],
"id": "ee73c6a8-12a8-57fc-ab34-81fd258667cc"
},
{
"name": "Known model?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.error ?? '' }}",
"rightValue": "unknown model",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"position": [
1100,
300
],
"id": "4ba2d3fa-9968-50d3-a29c-4c19ef3f7592"
},
{
"name": "Send report",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"position": [
1320,
300
],
"id": "c124f9d7-3b8c-5134-8343-936873621946"
},
{
"name": "Reject model",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 400
}
},
"position": [
1320,
520
],
"id": "95cf4c50-09ed-52f3-81e5-bfd72d87904b"
}
],
"connections": {
"Report webhook": {
"main": [
[
{
"node": "Get summary",
"type": "main",
"index": 0
}
]
]
},
"Get summary": {
"main": [
[
{
"node": "Get budget",
"type": "main",
"index": 0
}
]
]
},
"Get budget": {
"main": [
[
{
"node": "Get prices",
"type": "main",
"index": 0
}
]
]
},
"Get prices": {
"main": [
[
{
"node": "Make report",
"type": "main",
"index": 0
}
]
]
},
"Make report": {
"main": [
[
{
"node": "Known model?",
"type": "main",
"index": 0
}
]
]
},
"Known model?": {
"main": [
[
{
"node": "Send report",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject model",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
File diff suppressed because it is too large. Load diff