From 90d30593c4980ffccbf0cabe6d780cab9101e6f8 Mon Sep 17 00:00:00 2001 From: hywznn Date: Mon, 27 Jul 2026 15:52:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?experiment(intent):=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=A0=84=20=EB=9D=BC=EB=B2=A8=20provisional=20baseline=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predictions.jsonl | 268 ++++++++ .../provisional-char-ngram-nb-v1/report.yaml | 116 ++++ .../docs/INTENT_MODELING_HANDOFF.md | 1 + .../docs/INTENT_PROVISIONAL_BASELINE.md | 66 ++ fowoco-knowledge/knowledge/manifest.yaml | 2 + fowoco-knowledge/src/fowoco_knowledge/cli.py | 19 + .../fowoco_knowledge/provisional_baseline.py | 587 ++++++++++++++++++ .../src/fowoco_knowledge/validation.py | 94 +++ .../tests/test_provisional_baseline.py | 135 ++++ 9 files changed, 1288 insertions(+) create mode 100644 fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl create mode 100644 fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml create mode 100644 fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md create mode 100644 fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py create mode 100644 fowoco-knowledge/tests/test_provisional_baseline.py diff --git a/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl b/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl new file mode 100644 index 0000000..3a21760 --- /dev/null +++ b/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl @@ -0,0 +1,268 @@ +{"id":5,"hr_input":"WRK-190 내일부터 3라인 말고 5라인 가서 일하라고 해","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"내일부터 3라인 말고 5라인 가서 일하라고 해"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"내일부터 3라인 말고 5라인 가서 일하라고"}]},"structural_issues":[]} +{"id":6,"hr_input":"WRK-311님께 보호구 미착용시 작업장 출입 금지라고 공지해주세요","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"보호구 미착용시 작업장 출입 금지라고 공지"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"보호구 미착용시 작업장 출입 금지라고 공지해주세요"}]},"structural_issues":[]} +{"id":7,"hr_input":"WRK-056 이번주 토요일 특근 나오라고 전달","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번주 토요일 특근 나오라고 전달"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번주 토요일 특근 나오라고 전달"}]},"structural_issues":[]} +{"id":9,"hr_input":"WRK-129 점심시간 이후 창고정리 작업으로 재배치","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"점심시간 이후 창고정리 작업으로 재배치"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"작업으로 재배치"}]},"structural_issues":[]} +{"id":13,"hr_input":"WRK-091 내일 휴게시간 30분에서 1시간으로 변경 공지","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"휴게시간 30분에서 1시간으로 변경 공지"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"내일 휴게시간 30분에서 1시간으로 변경 공지"}]},"structural_issues":[]} +{"id":20,"hr_input":"WRK-155님 통장 사본 제출 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장 사본 제출 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장 사본 제출 부탁드립니다"}]},"structural_issues":[]} +{"id":25,"hr_input":"WRK-142 산업안전교육 이수증 받아서 인사팀에 전달","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"산업안전교육 이수증 받아서"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"이수증 받아서"}]},"structural_issues":[]} +{"id":30,"hr_input":"WRK-511님께 여권 원본 스캔본 받아 보내주세요","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 원본 스캔본 받아 보내주세요"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 원본 스캔본 받아 보내주세요"}]},"structural_issues":[]} +{"id":33,"hr_input":"WRK-233님 4대보험 공제액 상세내역 문의드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"4대보험 공제액 상세내역 문의드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"4대보험 공제액 상세내역 문의드립니다"}]},"structural_issues":[]} +{"id":36,"hr_input":"WRK-311 지각 3회로 인한 공제 내역 확인해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"지각 3회로 인한 공제 내역 확인"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"인한 공제 내역 확인해줘"}]},"structural_issues":[]} +{"id":37,"hr_input":"WRK-056님 명절상여금 지급 기준 문의","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"명절상여금 지급 기준 문의"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"명절상여금 지급 기준 문의"}]},"structural_issues":[]} +{"id":57,"hr_input":"WRK-792 산업안전보건교육 신규자 등록 처리 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규자 등록 처리 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규자 등록 처리"}]},"structural_issues":[]} +{"id":59,"hr_input":"WRK-914 신규 근로자 기숙사 배정 및 프로필 등록","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 근로자 기숙사 배정 및 프로필 등록"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"기숙사 배정 및 프로필 등록"}]},"structural_issues":[]} +{"id":68,"hr_input":"WRK-103 육아휴직 신청 접수 확인해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"육아휴직 신청 접수 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-103 육아휴직 신청 접수 확인해줘"}]},"structural_issues":[]} +{"id":69,"hr_input":"WRK-214님 연락 두절 5일째, 실종신고 검토 필요","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연락 두절 5일째, 실종신고 검토 필요"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연락 두절 5일째, 실종신고 검토 필요"}]},"structural_issues":[]} +{"id":71,"hr_input":"WRK-436님 병가 신청 승인 처리 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"병가 신청 승인 처리"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"병가 신청 승인 처리"}]},"structural_issues":[]} +{"id":77,"hr_input":"WRK-192 고용허가기간 만료 임박, 갱신 준비 진행","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 임박, 갱신 준비 진행"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 임박, 갱신"}]},"structural_issues":[]} +{"id":81,"hr_input":"WRK-636 고용허가 갱신 절차 이번주 안에 처리","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 갱신 절차 이번주 안에 처리"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 갱신 절차 이번주 안에"}]},"structural_issues":[]} +{"id":84,"hr_input":"WRK-969님 체류자격 연장 신청 일정 조율 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류자격 연장 신청 일정 조율"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류자격 연장 신청 일정 조율"}]},"structural_issues":[]} +{"id":86,"hr_input":"WRK-291님 근로계약 재계약 준비 진행해주세요","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 재계약 준비 진행해주세요"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 재계약 준비"}]},"structural_issues":[]} +{"id":92,"hr_input":"이번주 주말 날씨 어떤지 알아?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":96,"hr_input":"다음주 회식 장소 추천해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":101,"hr_input":"WRK-846 이름 발음 어떻게 하는지 궁금해서","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":111,"hr_input":"WRK-660님 컨베이어 속도 조정 관련 작업지침 전달 바랍니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"작업지침 전달 바랍니다"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"속도 조정 관련 작업지침 전달"}]},"structural_issues":[]} +{"id":115,"hr_input":"WRK-104님 다음주부터 2교대에서 3교대로 근무형태 변경 안내","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"3교대로 근무형태 변경 안내"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"WRK-104님 다음주부터 2교대에서 3교대로 근무형태"}]},"structural_issues":[]} +{"id":116,"hr_input":"WRK-215 오늘 오후 소방훈련 참여하라고 전달해줘","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 오후 소방훈련 참여하라고 전달해줘"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 오후 소방훈련 참여하라고 전달해줘"}]},"structural_issues":[]} +{"id":119,"hr_input":"WRK-548님 오늘 특정 구역 출입 통제 안내 전달해주세요","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"출입 통제 안내 전달"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"WRK-548님 오늘 특정 구역 출입 통제"}]},"structural_issues":[]} +{"id":123,"hr_input":"WRK-982님께 화재 대피 경로 숙지 안내 바랍니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"화재 대피 경로 숙지 안내"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"WRK-982님께 화재 대피 경로 숙지 안내"}]},"structural_issues":[]} +{"id":124,"hr_input":"WRK-193 이번달 남은기간 창고 입출고 담당으로 지정","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"창고 입출고 담당으로 지정"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"입출고 담당으로 지정"}]},"structural_issues":[]} +{"id":140,"hr_input":"WRK-869님 예방접종 증명서 받아 제출 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"예방접종 증명서 받아 제출"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"예방접종 증명서 받아 제출"}]},"structural_issues":[]} +{"id":141,"hr_input":"WRK-970 숙소 임대차계약서 사본 받아서 보관해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"임대차계약서 사본 받아서"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"임대차계약서 사본 받아서 보관해줘"}]},"structural_issues":[]} +{"id":160,"hr_input":"WRK-869 노동조합 가입확인서 받아서 인사파일에 등록","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"가입확인서 받아서"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"가입확인서 받아서"}]},"structural_issues":[]} +{"id":167,"hr_input":"WRK-536님께 상여금 지급일정 안내 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"상여금 지급일정 안내"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"상여금 지급일정"}]},"structural_issues":[]} +{"id":169,"hr_input":"WRK-758님 이번달 급여 이체일 문의드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 이체일 문의드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"이번달 급여 이체일 문의드립니다"}]},"structural_issues":[]} +{"id":196,"hr_input":"WRK-691님 무단결근 이틀째, 연락 안돼서 확인 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 이틀째, 연락 안돼서 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 이틀째, 연락 안돼서"}]},"structural_issues":[]} +{"id":199,"hr_input":"WRK-924 다음주부터 3일간 연차 사용 등록","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"3일간 연차 사용 등록"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"다음주부터 3일간"}]},"structural_issues":[]} +{"id":217,"hr_input":"WRK-702님 고용허가서 만료 임박, 연장 신청 진행 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가서 만료 임박, 연장 신청 진행"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가서 만료 임박, 연장 신청 진행"}]},"structural_issues":[]} +{"id":234,"hr_input":"WRK-368님이 요리를 잘한다는데 레시피 좀 물어봐줄까","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":236,"hr_input":"오늘 저녁 회식 메뉴 뭐가 좋을까","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":239,"hr_input":"이번 신제품 출시 일정 다시 확인해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"다시 확인해줘"}]},"structural_issues":[]} +{"id":242,"hr_input":"WRK-580 가족들 잘 지내는지 안부 좀 물어봐줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":245,"hr_input":"구내식당 메뉴 이번주 뭐 나오는지 알아?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":246,"hr_input":"WRK-702 성격이 어떤지 다들 어떻게 생각해?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-702 성격이"}]},"structural_issues":[]} +{"id":262,"hr_input":"WRK-328님께 최근 재직증명서 제출받아 정리 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 제출받아 정리 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 제출받아 정리"}]},"structural_issues":[]} +{"id":265,"hr_input":"WRK-651 근로계약서 갱신본 받아 대조 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 갱신본 받아 대조 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 갱신본 받아 대조"}]},"structural_issues":[]} +{"id":267,"hr_input":"WRK-873 사업장 변경 관련 사유서 받아서 첨부","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"사유서 받아서"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-873 사업장 변경"},{"intent":"DOCUMENT_REQUEST","evidence":"사유서 받아서 첨부"}]},"structural_issues":[]} +{"id":272,"hr_input":"WRK-328 급여계좌 변경 후 이체 확인 부탁드려","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 변경 후 이체 확인"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 변경 후 이체 확인 부탁드려"}]},"structural_issues":[]} +{"id":275,"hr_input":"WRK-651님께 급여 이중공제 의심건 확인 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 이중공제 의심건 확인"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 이중공제 의심건 확인"}]},"structural_issues":[]} +{"id":281,"hr_input":"첨부한 서류대로 WRK-217님 신규 등록 및 프로필 생성 진행해주세요","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 및 프로필 생성 진행"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 및 프로필 생성"}]},"structural_issues":[]} +{"id":284,"hr_input":"WRK-540 신규 입사 인사카드 작성 및 사번 부여","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 작성 및 사번 부여"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 작성 및 사번 부여"}]},"structural_issues":[]} +{"id":286,"hr_input":"WRK-762 신입 근로자 시스템 계정 최초 생성","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"시스템 계정 최초 생성"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"근로자 시스템 계정 최초 생성"}]},"structural_issues":[]} +{"id":288,"hr_input":"WRK-984 신규 근로자 기숙사 배정 초기 등록","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"기숙사 배정 초기 등록"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 근로자 기숙사 배정 초기 등록"}]},"structural_issues":[]} +{"id":303,"hr_input":"WRK-439님 근로계약 재계약 여부 이번주 확정 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 재계약 여부 이번주 확정"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 재계약 여부 이번주"}]},"structural_issues":[]} +{"id":304,"hr_input":"WRK-540 비자 만료까지 2주 남아 연장신청 서둘러줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료까지 2주 남아 연장신청 서둘러줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료까지 2주 남아 연장신청 서둘러줘"}]},"structural_issues":[]} +{"id":307,"hr_input":"WRK-873님 고용허가서 연장 신청 진행 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가서 연장 신청 진행 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가서 연장 신청 진행"}]},"structural_issues":[]} +{"id":313,"hr_input":"WRK-439 근로계약 만료 3주 전, 재계약 협의 진행","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 만료 3주 전, 재계약 협의 진행"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 만료 3주 전, 재계약 협의"}]},"structural_issues":[]} +{"id":318,"hr_input":"WRK-984님 비자 만료 임박, 출입국사무소 방문 일정 조율","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료 임박, 출입국사무소 방문 일정 조율"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료 임박,"}]},"structural_issues":[]} +{"id":319,"hr_input":"WRK-095 체류기간 연장허가 신청 접수 확인해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장허가 신청 접수 확인해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-095 체류기간 연장허가 신청 접수 확인해줘"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장허가"}]},"structural_issues":[]} +{"id":322,"hr_input":"WRK-328님 체류기간 연장 승인 여부 확인 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 승인 여부 확인"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 승인"}]},"structural_issues":[]} +{"id":323,"hr_input":"WRK-439 고용허가기간 만료 전 근로계약 재작성 진행","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 전 근로계약 재작성 진행"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 전 근로계약 재작성"}]},"structural_issues":[]} +{"id":330,"hr_input":"WRK-106님 근로계약 갱신 협의 일정 잡아주세요","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 갱신 협의 일정 잡아주세요"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 갱신 협의 일정"}]},"structural_issues":[]} +{"id":350,"hr_input":"WRK-106님 오늘 오후 작업 조기 마감 안내드립니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"작업 조기 마감 안내드립니다"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 오후 작업 조기 마감 안내드립니다"}]},"structural_issues":[]} +{"id":355,"hr_input":"WRK-651 건강검진표 최신본 받아서 등록해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"건강검진표 최신본 받아서"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"최신본 받아서"}]},"structural_issues":[]} +{"id":362,"hr_input":"WRK-328님께 근로계약서 서명본 원본 요청드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 서명본 원본 요청드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 서명본 원본 요청드립니다"}]},"structural_issues":[]} +{"id":365,"hr_input":"WRK-651 자격증 원본 대조 후 사본 받아둬","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"사본 받아둬"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"후 사본 받아둬"}]},"structural_issues":[]} +{"id":369,"hr_input":"WRK-095 재직증명서 발급 요청해서 받아줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 발급 요청해서 받아줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 발급 요청해서 받아줘"}]},"structural_issues":[]} +{"id":380,"hr_input":"WRK-106 이번달 결근 처리로 급여 삭감된 부분 확인","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"결근 처리로 급여 삭감된 부분 확인"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"처리로 급여 삭감된"}]},"structural_issues":[]} +{"id":382,"hr_input":"WRK-328님 특근수당 지급 기준 문의드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"특근수당 지급 기준 문의드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"특근수당 지급 기준 문의드립니다"}]},"structural_issues":[]} +{"id":385,"hr_input":"WRK-651 근속연수에 따른 수당 변동 확인해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"근속연수에 따른 수당 변동 확인해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"따른 수당 변동 확인해줘"}]},"structural_issues":[]} +{"id":394,"hr_input":"첨부 자료 기준 WRK-095님 건강보험 최초 가입 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"건강보험 최초 가입 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"건강보험 최초 가입 부탁드립니다"}]},"structural_issues":[]} +{"id":400,"hr_input":"업로드된 서류 기반 WRK-651님 인사카드 최초 작성 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 최초 작성 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 최초 작성 부탁드립니다"}]},"structural_issues":[]} +{"id":402,"hr_input":"WRK-328 다음달 5일자 퇴사 처리 진행해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"다음달 5일자 퇴사 처리 진행해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-328 다음달 5일자 퇴사 처리 진행해줘"}]},"structural_issues":[]} +{"id":403,"hr_input":"WRK-439님 사업장 변경 승인여부 확인 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 승인여부 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 승인여부"}]},"structural_issues":[]} +{"id":404,"hr_input":"WRK-540 이번주 연차 하루 사용 등록해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연차 하루 사용 등록해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-540 이번주 연차 하루 사용 등록해줘"}]},"structural_issues":[]} +{"id":410,"hr_input":"WRK-106 계약기간 만료로 퇴사처리 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"계약기간 만료로 퇴사처리 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-106 계약기간 만료로 퇴사처리"}]},"structural_issues":[]} +{"id":426,"hr_input":"오늘 미세먼지 심한데 마스크 챙겨야 하나","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":428,"hr_input":"WRK-984 술 잘마신다던데 회식때 조심해야겠어","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":434,"hr_input":"이번주 임원 회의 안건 정리해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"임원 회의 안건"}]},"structural_issues":[]} +{"id":437,"hr_input":"사내 게시판 공지사항 디자인 예쁘게 바꿔줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":440,"hr_input":"다음달 신제품 판매 전략 짜줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":441,"hr_input":"회사 주식 오늘 얼마인지 알아?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":449,"hr_input":"WRK-540 노래 잘한다던데 회식때 시켜볼까","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":454,"hr_input":"WRK-984 오늘부터 3라인 신규 라인장 보조 배치","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"신규 라인장 보조 배치"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘부터 3라인 신규 라인장 보조 배치"}]},"structural_issues":[]} +{"id":455,"hr_input":"WRK-095님께 기계 오작동시 즉시 정지 버튼 사용법 안내 부탁드립니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"정지 버튼 사용법 안내"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오작동시 즉시 정지 버튼 사용법 안내"}]},"structural_issues":[]} +{"id":459,"hr_input":"WRK-439 다음주부터 새 근무표대로 출근 지시","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"새 근무표대로 출근 지시"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"다음주부터 새 근무표대로 출근 지시"}]},"structural_issues":[]} +{"id":470,"hr_input":"WRK-540님 건강검진 결과지 제출받아 파일 정리 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"건강검진 결과지 제출받아 파일 정리 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"건강검진 결과지 제출받아 파일 정리 부탁드립니다"}]},"structural_issues":[]} +{"id":476,"hr_input":"WRK-106님께 이번달 실지급액 산출내역 설명 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"실지급액 산출내역 설명"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"실지급액 산출내역 설명 부탁드립니다"}]},"structural_issues":[]} +{"id":477,"hr_input":"wrk-217 급여명세서 못받았다는데 왜 그런지 확인 좀","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여명세서 못받았다는데 왜 그런지 확인 좀"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여명세서 못받았다는데 왜 그런지 확인 좀"}]},"structural_issues":[]} +{"id":478,"hr_input":"WRK-328님 시간외수당 과소지급 의심건 확인 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"시간외수당 과소지급 의심건 확인"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"시간외수당 과소지급 의심건 확인"}]},"structural_issues":[]} +{"id":479,"hr_input":"WRK-439 급여계좌 오류로 인한 미입금 확인해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 오류로 인한 미입금 확인해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 오류로 인한 미입금 확인해줘"}]},"structural_issues":[]} +{"id":482,"hr_input":"WRK-762 신입 근로자 고용보험 최초 취득신고","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"고용보험 최초 취득신고"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"고용보험 최초 취득신고"}]},"structural_issues":[]} +{"id":492,"hr_input":"WRK-762 이번달 말 계약해지 절차 진행해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"계약해지 절차 진행해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-762 이번달 말 계약해지 절차 진행해줘"}]},"structural_issues":[]} +{"id":517,"hr_input":"wrk-217 체류연장 신청서류 미리 챙겨놔줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"서류 미리 챙겨놔줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류연장 신청서류"}]},"structural_issues":[]} +{"id":521,"hr_input":"WRK-651 비자 만료까지 열흘 남아 연장절차 착수","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료까지 열흘 남아 연장절차 착수"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료까지 열흘 남아 연장절차"}]},"structural_issues":[]} +{"id":522,"hr_input":"WRK-762님 근로계약 종료 임박, 사업주 재계약 의사 확인 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 임박, 사업주 재계약 의사 확인"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 임박, 사업주 재계약 의사"}]},"structural_issues":[]} +{"id":523,"hr_input":"WRK-873 체류기간 연장 신청서 접수 여부 확인해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청서 접수 여부 확인해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청서"}]},"structural_issues":[]} +{"id":529,"hr_input":"WRK-439 외국인등록증 만료 다가와서 미리 갱신 신청해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"외국인등록증 만료 다가와서 미리 갱신 신청해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"외국인등록증 만료 다가와서 미리 갱신 신청해줘"}]},"structural_issues":[]} +{"id":534,"hr_input":"WRK-984님 근로계약 종료 두달 전, 연장 여부 조기 논의 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 두달 전, 연장 여부 조기 논의"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 두달 전, 연장 여부"}]},"structural_issues":[]} +{"id":539,"hr_input":"WRK-439 오늘 오전 원자재 검수 작업 우선 배치","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"원자재 검수 작업 우선 배치"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"원자재 검수 작업 우선 배치"}]},"structural_issues":[]} +{"id":550,"hr_input":"WRK-540 다음주 월요일 전 직원 안전교육 필참 지시","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"안전교육 필참 지시"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"직원 안전교육 필참 지시"}]},"structural_issues":[]} +{"id":554,"hr_input":"WRK-984 오늘 오후 특정구역 소음작업 진행 공지","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"소음작업 진행 공지"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 오후 특정구역 소음작업 진행 공지"}]},"structural_issues":[]} +{"id":563,"hr_input":"WRK-873님께 외국인등록증 사본 재요청드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"외국인등록증 사본 재요청드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"외국인등록증 사본 재요청드립니다"}]},"structural_issues":[]} +{"id":568,"hr_input":"WRK-328님께 임금대장 서명본 다시 요청드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"임금대장 서명본 다시 요청드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"임금대장 서명본 다시 요청드립니다"}]},"structural_issues":[]} +{"id":569,"hr_input":"WRK-439 숙소계약서 사본 재발급받아서 등록해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재발급받아서"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"숙소계약서 사본 재발급받아서"}]},"structural_issues":[]} +{"id":575,"hr_input":"WRK-095 근로계약서 서명본 다시 받아서 대조해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"서명본 다시 받아서 대조해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 서명본 다시 받아서 대조해줘"}]},"structural_issues":[]} +{"id":592,"hr_input":"WRK-762님 4대보험 정산금 관련 안내 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"4대보험 정산금 관련 안내"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"정산금 관련 안내 부탁드립니다"}]},"structural_issues":[]} +{"id":604,"hr_input":"WRK-984 신규 입사자 국민연금 최초 가입 처리","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"국민연금 최초 가입 처리"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 입사자 국민연금 최초 가입 처리"}]},"structural_issues":[]} +{"id":605,"hr_input":"첨부한 계약서 기반 WRK-095님 급여계좌 초기 등록 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"급여계좌 초기 등록 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"급여계좌 초기 등록 부탁드립니다"}]},"structural_issues":[]} +{"id":606,"hr_input":"WRK-106 신규 근로자 산재보험 최초 취득 처리","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"산재보험 최초 취득 처리"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"근로자 산재보험 최초 취득 처리"}]},"structural_issues":[]} +{"id":620,"hr_input":"업로드된 서류대로 WRK-540님 인사카드 및 기숙사 초기 등록 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 및 기숙사 초기 등록 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 및 기숙사 초기 등록 부탁드립니다"}]},"structural_issues":[]} +{"id":623,"hr_input":"WRK-873님 사업장 변경 접수 상태 확인 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 접수 상태 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 접수 상태 확인"}]},"structural_issues":[]} +{"id":632,"hr_input":"WRK-762님 출산휴가 신청 접수 및 반영 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"출산휴가 신청 접수 및 반영 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"출산휴가 신청 접수 및 반영"}]},"structural_issues":[]} +{"id":636,"hr_input":"WRK-106님 결혼휴가 신청 접수 및 반영 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"결혼휴가 신청 접수 및 반영 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"결혼휴가 신청 접수 및 반영"}]},"structural_issues":[]} +{"id":638,"hr_input":"WRK-328님 무단결근 사흘째, 이직확인서 준비 필요 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 사흘째, 이직확인서 준비 필요"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 사흘째, 이직확인서"}]},"structural_issues":[]} +{"id":639,"hr_input":"WRK-439 사업장 이동 승인 여부 확인해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 이동 승인 여부 확인해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-439 사업장 이동"}]},"structural_issues":[]} +{"id":641,"hr_input":"이번주 신제품 출시 이벤트 아이디어 좀 줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":642,"hr_input":"요즘 볼만한 드라마 추천해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":647,"hr_input":"이번달 생산 목표치 그래프로 정리해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":648,"hr_input":"WRK-762 형제자매가 몇명인지 궁금하네","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":650,"hr_input":"이번주 환율 전망 어떻게 되는지 알아?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":653,"hr_input":"회사 앞 편의점 몇시까지 열어?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":664,"hr_input":"이번주 프로젝트 마감일 다시 확인해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"다시 확인해줘"}]},"structural_issues":[]} +{"id":670,"hr_input":"요즘 뜨는 주식 종목 알려줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":675,"hr_input":"다음주 신제품 시연회 준비물 정리해줘","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"신제품 시연회"}]},"structural_issues":[]} +{"id":676,"hr_input":"이번주 서울 날씨 어때?","expected":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"predicted":{"intents":[{"intent":"OUT_OF_SCOPE","evidence":null}]},"structural_issues":[]} +{"id":682,"hr_input":"WRK-984 다음주부터 냉동창고 근무로 이동","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"냉동창고 근무로 이동"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"다음주부터 냉동창고 근무로 이동"}]},"structural_issues":[]} +{"id":687,"hr_input":"WRK-439님께 이번주 위험구역 접근금지 안내드립니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"위험구역 접근금지 안내드립니다"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"WRK-439님께 이번주 위험구역 접근금지 안내드립니다"}]},"structural_issues":[]} +{"id":692,"hr_input":"WRK-984님께 화학물질 누출시 대응요령 안내드립니다","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"대응요령 안내드립니다"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"안내드립니다"}]},"structural_issues":[]} +{"id":700,"hr_input":"WRK-762님 근로계약서 서명본 받아서 인사팀 전달 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"서명본 받아서 인사팀 전달 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 서명본 받아서"}]},"structural_issues":[]} +{"id":702,"hr_input":"WRK-984님께 고용허가서 사본 재확보 요청드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"고용허가서 사본 재확보 요청드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"고용허가서 사본 재확보 요청드립니다"}]},"structural_issues":[]} +{"id":714,"hr_input":"WRK-106 연장근로수당 산정기준 다시 설명해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"연장근로수당 산정기준 다시 설명해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"연장근로수당 산정기준 다시 설명해줘"}]},"structural_issues":[]} +{"id":720,"hr_input":"WRK-762 근태 마감 후 급여 재반영 확인해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 재반영 확인해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"근태 마감 후 급여 재반영 확인해줘"}]},"structural_issues":[]} +{"id":721,"hr_input":"첨부한 계약서 기반 WRK-873님 신규 등록 및 급여계좌 초기 등록 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 및 급여계좌 초기 등록 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 및 급여계좌 초기 등록"}]},"structural_issues":[]} +{"id":724,"hr_input":"WRK-106 신규 입사자 고용보험 최초 취득 처리","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"고용보험 최초 취득 처리"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 입사자 고용보험 최초 취득 처리"}]},"structural_issues":[]} +{"id":726,"hr_input":"WRK-328 신규 근로자 기숙사 배정 및 프로필 등록","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"기숙사 배정 및 프로필 등록"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"기숙사 배정 및 프로필 등록"}]},"structural_issues":[]} +{"id":727,"hr_input":"업로드된 계약서 참고 WRK-439님 건강보험 최초 가입 부탁드립니다","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"건강보험 최초 가입 부탁드립니다"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"건강보험 최초 가입 부탁드립니다"}]},"structural_issues":[]} +{"id":728,"hr_input":"WRK-540 신입 근로자 사원증 발급 및 시스템 등록","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"사원증 발급 및 시스템 등록"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"근로자 사원증 발급 및 시스템 등록"}]},"structural_issues":[]} +{"id":739,"hr_input":"WRK-651 이번주 연차 하루 사용 등록해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연차 하루 사용 등록해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-651 이번주 연차 하루 사용 등록해줘"}]},"structural_issues":[]} +{"id":741,"hr_input":"WRK-873 사업장 변경 승인여부 확인해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 승인여부 확인해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-873 사업장 변경 승인여부"}]},"structural_issues":[]} +{"id":750,"hr_input":"WRK-762님 퇴사 후 정산 절차 진행 확인 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 후 정산 절차 진행 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 후 정산 절차 진행 확인"}]},"structural_issues":[]} +{"id":754,"hr_input":"WRK-106님 비자 만료일 다가와서 연장신청 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료일 다가와서 연장신청 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"비자 만료일 다가와서 연장신청"}]},"structural_issues":[]} +{"id":757,"hr_input":"WRK-439 근로계약 종료 임박, 재계약 여부 논의해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"재계약 여부 논의해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 임박, 재계약 여부 논의해줘"}]},"structural_issues":[]} +{"id":769,"hr_input":"WRK-651 오늘 오전 기계 점검으로 작업 지연 공지","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"작업 지연 공지"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 오전 기계 점검으로 작업 지연"}]},"structural_issues":[]} +{"id":773,"hr_input":"WRK-095 이번주 남은기간 포장검수 담당 재배치","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"포장검수 담당 재배치"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"포장검수 담당 재배치"}]},"structural_issues":[]} +{"id":777,"hr_input":"WRK-439님 재직증명서 신규 발급받아 제출 부탁드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 신규 발급받아 제출 부탁드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"재직증명서 신규 발급받아 제출 부탁드립니다"}]},"structural_issues":[]} +{"id":779,"hr_input":"WRK-651님께 외국인등록증 사본 요청드립니다","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"외국인등록증 사본 요청드립니다"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"외국인등록증 사본 요청드립니다"}]},"structural_issues":[]} +{"id":782,"hr_input":"WRK-984 고용허가서 사본 받아서 갱신 신청에 첨부","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"고용허가서 사본 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"갱신 신청에 첨부"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"고용허가서 사본 받아서 갱신"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가서 사본 받아서 갱신 신청에"}]},"structural_issues":[]} +{"id":787,"hr_input":"WRK-439님께 급여 이체 오류 관련 확인 부탁드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 이체 오류 관련 확인 부탁드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 이체 오류 관련"}]},"structural_issues":[]} +{"id":792,"hr_input":"WRK-984 이번달 결근 공제 재확인해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"결근 공제 재확인해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"이번달 결근 공제 재확인해줘"}]},"structural_issues":[]} +{"id":793,"hr_input":"WRK-095님 상여금 미지급 사유 재문의드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"상여금 미지급 사유 재문의드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"상여금 미지급 사유 재문의드립니다"}]},"structural_issues":[]} +{"id":795,"hr_input":"WRK-217님께 세금 원천징수 재계산 요청드립니다","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"세금 원천징수 재계산 요청드립니다"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"세금 원천징수 재계산 요청드립니다"}]},"structural_issues":[]} +{"id":799,"hr_input":"WRK-651 신규 입사자 국민연금 최초 가입 처리","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"국민연금 최초 가입 처리"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 입사자 국민연금 최초 가입 처리"}]},"structural_issues":[]} +{"id":801,"hr_input":"WRK-873 신규 근로자 산재보험 최초 취득 처리","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"산재보험 최초 취득 처리"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"근로자 산재보험 최초 취득 처리"}]},"structural_issues":[]} +{"id":804,"hr_input":"첨부한 외국인등록증으로 WRK-106 인사카드 신규 작성해줘","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 신규 작성해줘"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"인사카드 신규 작성해줘"}]},"structural_issues":[]} +{"id":810,"hr_input":"WRK-762 이번주 연차 이틀 사용 등록해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연차 이틀 사용 등록해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-762 이번주 연차 이틀 사용 등록해줘"}]},"structural_issues":[]} +{"id":812,"hr_input":"WRK-984 사업장 변경 승인여부 확인해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 승인여부 확인해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-984 사업장 변경 승인여부"}]},"structural_issues":[]} +{"id":814,"hr_input":"WRK-106 육아휴직 신청 접수 및 반영해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"육아휴직 신청 접수 및 반영해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-106 육아휴직 신청 접수 및 반영해줘"}]},"structural_issues":[]} +{"id":817,"hr_input":"WRK-439님 병가 신청 접수 및 반영 부탁드립니다","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"병가 신청 접수 및 반영 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"병가 신청 접수 및 반영"}]},"structural_issues":[]} +{"id":818,"hr_input":"WRK-540 사업장 이동 후 재직상태 업데이트해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"재직상태 업데이트해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-540 사업장 이동 후 재직상태 업데이트해줘"}]},"structural_issues":[]} +{"id":826,"hr_input":"WRK-328 고용허가 갱신 절차 이번주 안에 처리해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 갱신 절차 이번주 안에 처리해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 갱신 절차 이번주 안에"}]},"structural_issues":[]} +{"id":827,"hr_input":"WRK-439님 근로계약 종료 임박, 재계약 여부 논의 부탁드립니다","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 임박, 재계약 여부 논의 부탁드립니다"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 종료 임박, 재계약 여부 논의"}]},"structural_issues":[]} +{"id":841,"hr_input":"WRK-101 여권 사본과 외국인등록증을 받아서 신규 등록을 진행해주세요.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 사본과 외국인등록증을 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록을 진행"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 사본과 외국인등록증을 받아서 신규"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록을"}]},"structural_issues":[]} +{"id":845,"hr_input":"WRK-105 통장사본 좀 받아서 등록 마무리하자","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 좀 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록 마무리하자"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 좀 받아서"}]},"structural_issues":[]} +{"id":847,"hr_input":"WRK-107 계약서와 여권 사본을 받아 신규 근로자 등록을 완료해주시기 바랍니다.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서와 여권 사본을 받아"},{"intent":"WORKER_ONBOARDING","evidence":"신규 근로자 등록을 완료"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서와 여권 사본을 받아"},{"intent":"WORKER_ONBOARDING","evidence":"신규 근로자 등록을"}]},"structural_issues":[]} +{"id":852,"hr_input":"WRK-112 여권사본 제출받아 신규등록 진행","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권사본 제출받아"},{"intent":"WORKER_ONBOARDING","evidence":"신규등록 진행"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권사본 제출받아"}]},"structural_issues":[]} +{"id":864,"hr_input":"WRK-124 여권, 등록증 제출받아 신규 프로필 생성","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권, 등록증 제출받아"},{"intent":"WORKER_ONBOARDING","evidence":"신규 프로필 생성"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권, 등록증 제출받아"},{"intent":"WORKER_ONBOARDING","evidence":"신규 프로필 생성"}]},"structural_issues":[]} +{"id":879,"hr_input":"WRK-209: 외국인등록증 확보 후 체류기간 연장 신청","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"외국인등록증 확보"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청"}]},"structural_issues":[]} +{"id":880,"hr_input":"WRK-210의 계약서 서명본과 여권을 받아 재계약 절차를 진행해주시기 바랍니다.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서 서명본과 여권을 받아"},{"intent":"EXPIRY_RENEWAL","evidence":"재계약 절차를 진행"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서 서명본과 여권을 받아"},{"intent":"EXPIRY_RENEWAL","evidence":"받아 재계약 절차를"}]},"structural_issues":[]} +{"id":886,"hr_input":"WRK-216의 등록증 사본을 받아서 근로계약 갱신을 준비해주세요.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 사본을 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 갱신을 준비"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 사본을 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"근로계약 갱신을"}]},"structural_issues":[]} +{"id":889,"hr_input":"고용허가 연장을 앞둔 WRK-219의 여권 사본을 받아서 서류를 준비하겠습니다.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 사본을 받아서 서류를 준비"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 연장을"},{"intent":"DOCUMENT_REQUEST","evidence":"여권 사본을 받아서 서류를"}]},"structural_issues":[]} +{"id":892,"hr_input":"WRK-222의 여권과 계약서를 받아서 체류기간 연장 신청을 접수하겠습니다.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서를 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청을 접수"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서를 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장 신청을"}]},"structural_issues":[]} +{"id":893,"hr_input":"WRK-223 여권 받아서 비자 갱신 준비해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"비자 갱신 준비해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"비자 갱신 준비해줘"}]},"structural_issues":[]} +{"id":894,"hr_input":"WRK-224 등록증 제출받아 고용허가 연장 진행","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 제출받아"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 연장 진행"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 제출받아"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 연장 진행"}]},"structural_issues":[]} +{"id":899,"hr_input":"WRK-229 서명본 받아서 재계약 마무리하자","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"서명본 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"재계약 마무리하자"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"서명본 받아서 재계약"},{"intent":"EXPIRY_RENEWAL","evidence":"받아서 재계약 마무리하자"}]},"structural_issues":[]} +{"id":902,"hr_input":"WRK-302 출퇴근기록 받아서 수당 계산해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"출퇴근기록 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"수당 계산해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"출퇴근기록 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"수당 계산해줘"}]},"structural_issues":[]} +{"id":908,"hr_input":"WRK-308 급여명세서 받아서 공제 항목 알려줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"공제 항목 알려줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서 공제"},{"intent":"PAYROLL_EXPLANATION","evidence":"받아서 공제 항목 알려줘"}]},"structural_issues":[]} +{"id":915,"hr_input":"WRK-315: 통장사본 받아 급여 지급일 안내","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 지급일 안내"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아"},{"intent":"PAYROLL_EXPLANATION","evidence":"받아 급여 지급일 안내"}]},"structural_issues":[]} +{"id":920,"hr_input":"WRK-320 초과근무확인서 받아서 야근수당 계산해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"초과근무확인서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"야근수당 계산해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"초과근무확인서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"야근수당 계산해줘"}]},"structural_issues":[]} +{"id":932,"hr_input":"WRK-402 무단결근이야, 대신 WRK-403 오늘 야간조로 돌려줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-402 무단결근이야"},{"intent":"WORK_INSTRUCTION","evidence":"오늘 야간조로 돌려줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-402 무단결근이야, 대신 WRK-403"},{"intent":"WORK_INSTRUCTION","evidence":"오늘 야간조로"}]},"structural_issues":[]} +{"id":934,"hr_input":"WRK-405의 사업장 변경이 확정되어, 잔여 인원은 포장 라인으로 재배치 바랍니다.","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경이 확정"},{"intent":"WORK_INSTRUCTION","evidence":"포장 라인으로 재배치"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경이 확정되어,"},{"intent":"WORK_INSTRUCTION","evidence":"포장 라인으로 재배치"}]},"structural_issues":[]} +{"id":948,"hr_input":"WRK-424 연락두절, 야간조 작업지시 재조정 필요","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연락두절"},{"intent":"WORK_INSTRUCTION","evidence":"야간조 작업지시 재조정"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-424 연락두절,"},{"intent":"WORK_INSTRUCTION","evidence":"야간조 작업지시 재조정 필요"}]},"structural_issues":[]} +{"id":967,"hr_input":"WRK-507이 무단결근하였고, 체류기간 만료가 임박하여 강제 출국 여부를 검토해야 합니다.","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근하였고"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 만료가 임박하여 강제 출국 여부를 검토"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근하였고,"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 만료가 임박하여"}]},"structural_issues":[]} +{"id":970,"hr_input":"WRK-510의 퇴사가 결정되었으며, 이는 고용허가기간 만료 시점과 맞물려 있습니다.","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사가 결정"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 시점과 맞물려"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-510의 퇴사가 결정되었으며,"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료 시점과"}]},"structural_issues":[]} +{"id":974,"hr_input":"WRK-514 사업장 옮긴다는데 고용허가 연장도 같이 처리해야 해","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-514 사업장 옮긴다는데"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 연장도 같이 처리해야 해"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-514 사업장 옮긴다는데"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가 연장도"}]},"structural_issues":[]} +{"id":980,"hr_input":"WRK-520 퇴사한다는데 체류기간은 아직 안 끝났어, 출국일 확인해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-520 퇴사한다는데"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간은 아직 안 끝났어, 출국일 확인해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-520 퇴사한다는데"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간은 아직 안"}]},"structural_issues":[]} +{"id":988,"hr_input":"WRK-528의 퇴사 절차가 진행 중이며, 체류기간 만료와 시점이 겹쳐 출국 일정을 조율해야 합니다.","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 절차가 진행 중"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 만료와 시점이 겹쳐 출국 일정을 조율"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 절차가 진행 중이며,"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 만료와 시점이 겹쳐 출국 일정을"}]},"structural_issues":[]} +{"id":994,"hr_input":"WRK-604 재계약 준비 중인데, 계약서 받아서 이번 달 급여 인상분도 같이 반영해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"재계약 준비 중"},{"intent":"DOCUMENT_REQUEST","evidence":"계약서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 인상분도 같이 반영"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"재계약 준비"},{"intent":"DOCUMENT_REQUEST","evidence":"계약서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"이번 달 급여 인상분도"}]},"structural_issues":[]} +{"id":1002,"hr_input":"WRK-612 통장사본 받아서 급여계좌 변경 처리해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 변경 처리해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"받아서 급여계좌 변경 처리해줘"}]},"structural_issues":[]} +{"id":1005,"hr_input":"WRK-615 오늘 지게차 작업 배정할게","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 지게차 작업 배정할게"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 지게차 작업 배정할게"}]},"structural_issues":[]} +{"id":1010,"hr_input":"WRK-620 이번 주 특근 스케줄 잡을게, 특근수당도 정확히 계산해서 알려줘","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번 주 특근 스케줄 잡을게"},{"intent":"PAYROLL_EXPLANATION","evidence":"특근수당도 정확히 계산해서 알려줘"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번 주 특근 스케줄 잡을게, 특근수당도"},{"intent":"PAYROLL_EXPLANATION","evidence":"특근수당도 정확히 계산해서 알려줘"}]},"structural_issues":[]} +{"id":1011,"hr_input":"WRK-621 여권 만료 임박이라 갱신 서류 받아서 접수해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"여권 만료 임박이라 갱신"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"여권 만료 임박이라 갱신 서류"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수해줘"}]},"structural_issues":[]} +{"id":1013,"hr_input":"WRK-623 건강진단서 받아서 등록 처리하고, 배치 라인도 정해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"건강진단서 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록 처리"},{"intent":"WORK_INSTRUCTION","evidence":"배치 라인도 정해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"건강진단서 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"받아서 등록 처리하고,"}]},"structural_issues":[]} +{"id":1019,"hr_input":"WRK-629 오늘 결근이야, 대체 인력 배치하고 결근 사유 파악해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 결근이야"},{"intent":"WORK_INSTRUCTION","evidence":"대체 인력 배치"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-629 오늘 결근이야,"},{"intent":"WORK_INSTRUCTION","evidence":"대체 인력 배치하고"}]},"structural_issues":[]} +{"id":1023,"hr_input":"WRK-633 오전에 결근했다가 오후 복귀함, 반차 처리 후 근무표 조정해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오전에 결근했다가 오후 복귀함"},{"intent":"WORK_INSTRUCTION","evidence":"근무표 조정해줘"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"반차 처리 후 근무표 조정해줘"}]},"structural_issues":[]} +{"id":1024,"hr_input":"신규 등록 예정인 WRK-634의 여권과 계약서를 받아서 시스템에 올려주세요.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서를 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"시스템에 올려주세요"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 예정인"},{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서를 받아서"}]},"structural_issues":[]} +{"id":1028,"hr_input":"WRK-638 오늘 컨베이어 점검 작업 배정할게","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 컨베이어 점검 작업 배정할게"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"점검 작업 배정할게"}]},"structural_issues":[]} +{"id":1037,"hr_input":"WRK-647 급여명세서 확인 요청 들어왔는데 공제 내역 좀 설명해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여명세서 확인 요청 들어왔는데 공제 내역 좀 설명해줘"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"들어왔는데 공제 내역 좀 설명해줘"}]},"structural_issues":[]} +{"id":1042,"hr_input":"WRK-652 등록증 받아서 신규 등록하고 배치는 나중에 결정할게","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 받아서 신규"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록하고"}]},"structural_issues":[]} +{"id":1046,"hr_input":"WRK-656 다음 주부터 야간조 전환, 야간수당 기준 미리 공지해줘","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"다음 주부터 야간조 전환"},{"intent":"PAYROLL_EXPLANATION","evidence":"야간수당 기준 미리 공지해줘"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"다음 주부터 야간조 전환,"},{"intent":"PAYROLL_EXPLANATION","evidence":"야간수당 기준 미리"}]},"structural_issues":[]} +{"id":1049,"hr_input":"신규 WRK-659 보험 최초 가입 처리해줘, 서류는 첨부되어 있음","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"보험 최초 가입 처리해줘"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"보험 최초 가입 처리해줘,"}]},"structural_issues":[]} +{"id":1051,"hr_input":"WRK-661 근로계약서 받아서 갱신 준비하고, 급여 인상률도 함께 확인해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"갱신 준비"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 인상률도 함께 확인해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"근로계약서 받아서 갱신"},{"intent":"EXPIRY_RENEWAL","evidence":"근로계약서 받아서 갱신 준비하고,"}]},"structural_issues":[]} +{"id":1059,"hr_input":"WRK-669 계약만료일 다가와서 갱신 서류 받아서 미리 준비해줘","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료일 다가와서 갱신"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 미리 준비해줘"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료일 다가와서 갱신 서류"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 미리"}]},"structural_issues":[]} +{"id":1060,"hr_input":"WRK-670 급여 오류 정정 요청 들어왔어, 확인해서 처리해줘","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 오류 정정 요청 들어왔어"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 오류 정정 요청"}]},"structural_issues":[]} +{"id":1062,"hr_input":"WRK-672: 사진, 여권 받아서 신규 등록 처리 완료함","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"사진, 여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 처리 완료"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"사진, 여권 받아서 신규"},{"intent":"WORKER_ONBOARDING","evidence":"신규 등록 처리"}]},"structural_issues":[]} +{"id":1065,"hr_input":"WRK-675 급여 명세서에 오류 있다고 컴플레인 왔어요, 확인 후 안내 부탁드립니다.","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세서에 오류 있다고 컴플레인 왔어요"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세서에 오류 있다고"}]},"structural_issues":[]} +{"id":1075,"hr_input":"WRK-685: 신규입사, 등록 및 안전교육 배치 동시 진행 요망","expected":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규입사, 등록"},{"intent":"WORK_INSTRUCTION","evidence":"안전교육 배치 동시 진행"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"및 안전교육 배치 동시"}]},"structural_issues":[]} +{"id":1079,"hr_input":"WRK-689 급여 지급 방식 문의, 계좌이체 vs 현금 여부 설명 필요","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 지급 방식 문의, 계좌이체 vs 현금 여부 설명 필요"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 지급 방식 문의, 계좌이체"}]},"structural_issues":[]} +{"id":1080,"hr_input":"WRK-690 사업장 변경 확정, 새 근무지 작업 안내 및 급여체계 설명 동시 진행","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 확정"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 작업 안내"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여체계 설명 동시 진행"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-690 사업장 변경 확정,"},{"intent":"WORK_INSTRUCTION","evidence":"확정, 새 근무지 작업 안내"}]},"structural_issues":[]} +{"id":1091,"hr_input":"WRK-701 사업장 변경 서류 받아서 정리하고, 새 근무지 작업 배치까지 확인 부탁","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 정리"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 작업 배치까지 확인"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-701 사업장 변경 서류"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 정리하고,"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 작업 배치까지"}]},"structural_issues":[]} +{"id":1094,"hr_input":"WRK-704 무단결근 3일째, 자동 퇴사 처리 검토 및 대체인력 배치 시급함","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 3일째, 자동 퇴사 처리 검토"},{"intent":"WORK_INSTRUCTION","evidence":"대체인력 배치 시급함"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-704 무단결근 3일째, 자동 퇴사 처리"},{"intent":"WORK_INSTRUCTION","evidence":"대체인력 배치"}]},"structural_issues":[]} +{"id":1099,"hr_input":"WRK-709 휴가 신청 승인했어, 대체 근무자 배치해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"휴가 신청 승인했어"},{"intent":"WORK_INSTRUCTION","evidence":"대체 근무자 배치해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-709 휴가 신청 승인했어,"},{"intent":"WORK_INSTRUCTION","evidence":"대체 근무자 배치해줘"}]},"structural_issues":[]} +{"id":1102,"hr_input":"WRK-712 사업장 변경 후 새 근무지 배치 및 급여체계 안내까지 마무리해줘","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 배치"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여체계 안내까지 마무리해줘"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-712 사업장 변경 후"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 배치 및"},{"intent":"PAYROLL_EXPLANATION","evidence":"및 급여체계 안내까지"}]},"structural_issues":[]} +{"id":1104,"hr_input":"WRK-714 이번 주 특근 없음, 근무표 확인해줘","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번 주 특근 없음, 근무표 확인해줘"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"이번 주 특근 없음, 근무표 확인해줘"}]},"structural_issues":[]} +{"id":1116,"hr_input":"WRK-726 급여명세서 받아서 공제항목 확인 부탁","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"공제항목 확인 부탁"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여명세서 받아서 공제항목 확인 부탁"},{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서"}]},"structural_issues":[]} +{"id":1118,"hr_input":"신규 WRK-728 여권 받아서 등록하고 보험 최초 가입 및 배치 라인까지 함께 진행해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록하고 보험 최초 가입"},{"intent":"WORK_INSTRUCTION","evidence":"배치 라인까지 함께 진행해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"보험 최초 가입 및"}]},"structural_issues":[]} +{"id":1122,"hr_input":"WRK-732 여권과 등록증 받아서 연장 신청, 접수 결과는 급여명세서와 함께 안내해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 등록증 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"연장 신청"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여명세서와 함께 안내해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 등록증 받아서 연장"},{"intent":"EXPIRY_RENEWAL","evidence":"받아서 연장 신청, 접수"}]},"structural_issues":[]} +{"id":1123,"hr_input":"WRK-733: 오늘 라인 배치 그대로 유지","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 그대로 유지"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 그대로 유지"}]},"structural_issues":[]} +{"id":1124,"hr_input":"WRK-734 퇴사 처리하고 남은 라인 인력 재배치 지시함","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 처리"},{"intent":"WORK_INSTRUCTION","evidence":"남은 라인 인력 재배치 지시함"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-734 퇴사 처리하고"},{"intent":"WORK_INSTRUCTION","evidence":"남은 라인 인력 재배치 지시함"}]},"structural_issues":[]} +{"id":1125,"hr_input":"WRK-735 급여 명세 확인차 통장사본 받아서 대조 부탁드립니다.","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세 확인차"},{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세 확인차"},{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서 대조"}]},"structural_issues":[]} +{"id":1130,"hr_input":"WRK-740 오늘 지시대로 조립라인 이동 완료","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 지시대로 조립라인 이동 완료"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 지시대로 조립라인 이동"}]},"structural_issues":[]} +{"id":1135,"hr_input":"WRK-745 사업장 변경 서류 받아서 접수, 새 근무지 배치는 회사 측 지시 대기 중","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-745 사업장 변경 서류"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수,"},{"intent":"WORK_INSTRUCTION","evidence":"새 근무지 배치는"}]},"structural_issues":[]} +{"id":1139,"hr_input":"WRK-749 퇴사 예정, 마지막 근무일 작업지시 및 급여정산 함께 진행","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 예정"},{"intent":"WORK_INSTRUCTION","evidence":"마지막 근무일 작업지시"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여정산 함께 진행"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-749 퇴사 예정,"},{"intent":"PAYROLL_EXPLANATION","evidence":"마지막 근무일 작업지시 및 급여정산 함께"},{"intent":"WORK_INSTRUCTION","evidence":"근무일 작업지시 및"}]},"structural_issues":[]} +{"id":1140,"hr_input":"WRK-750: 오늘 작업배치 문의만 있었음","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업배치 문의만 있었음"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업배치 문의만 있었음"}]},"structural_issues":[]} +{"id":1142,"hr_input":"WRK-752 등록증 받아서 갱신 접수 완료, 급여 관련 문의는 없었음","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"갱신 접수 완료"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증 받아서 갱신"}]},"structural_issues":[]} +{"id":1154,"hr_input":"WRK-764: 오늘부터 신규 검사공정 배치","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘부터 신규 검사공정 배치"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘부터 신규 검사공정 배치"}]},"structural_issues":[]} +{"id":1155,"hr_input":"WRK-765 계약만료 임박, 갱신 서류 받아서 접수하고 결과는 다음 주 안내 예정","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료 임박, 갱신"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료 임박, 갱신 서류"},{"intent":"DOCUMENT_REQUEST","evidence":"서류 받아서 접수하고"}]},"structural_issues":[]} +{"id":1157,"hr_input":"WRK-767 급여명세서 받아서 4대보험 공제 확인해줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"4대보험 공제 확인해줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"급여명세서 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"4대보험 공제 확인해줘"}]},"structural_issues":[]} +{"id":1160,"hr_input":"WRK-770 오늘 결근, 사업장 변경 신청도 함께 접수됨","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 결근, 사업장 변경 신청도 함께 접수됨"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-770 오늘 결근, 사업장 변경 신청도"},{"intent":"WORK_INSTRUCTION","evidence":"WRK-770 오늘 결근,"}]},"structural_issues":[]} +{"id":1162,"hr_input":"WRK-772: 오늘 라인 작업 지시 전달함","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 작업 지시 전달함"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 작업 지시 전달함"}]},"structural_issues":[]} +{"id":1163,"hr_input":"WRK-773 계약서와 여권 받아서 재계약, 배치 라인은 그대로 유지 예정","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서와 여권 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"재계약"},{"intent":"WORK_INSTRUCTION","evidence":"배치 라인은 그대로 유지 예정"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서와 여권 받아서 재계약,"},{"intent":"EXPIRY_RENEWAL","evidence":"받아서 재계약,"},{"intent":"WORK_INSTRUCTION","evidence":"재계약, 배치 라인은 그대로 유지"}]},"structural_issues":[]} +{"id":1169,"hr_input":"WRK-779: 오늘 작업지시서 배포, 안전화 착용 확인","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업지시서 배포, 안전화 착용 확인"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업지시서 배포, 안전화 착용"}]},"structural_issues":[]} +{"id":1170,"hr_input":"WRK-780 퇴사 처리 완료, 마지막 급여 정산 및 인수인계 작업지시 함께 처리","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"퇴사 처리 완료"},{"intent":"PAYROLL_EXPLANATION","evidence":"마지막 급여 정산"},{"intent":"WORK_INSTRUCTION","evidence":"인수인계 작업지시 함께 처리"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-780 퇴사 처리"},{"intent":"PAYROLL_EXPLANATION","evidence":"마지막 급여 정산 및"},{"intent":"WORK_INSTRUCTION","evidence":"인수인계 작업지시"}]},"structural_issues":[]} +{"id":1175,"hr_input":"신규 WRK-785 여권 받아서 등록하고, 배치 라인 확정 시 급여계좌도 함께 설정 부탁","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록"},{"intent":"WORK_INSTRUCTION","evidence":"배치 라인 확정"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌도 함께 설정 부탁"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 WRK-785 여권 받아서 등록하고, 배치"},{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"}]},"structural_issues":[]} +{"id":1187,"hr_input":"WRK-797: 오늘 새 작업지시서 배포, 라인장 서명 확인 필요","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 작업지시서 배포, 라인장 서명 확인 필요"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 작업지시서 배포, 라인장"}]},"structural_issues":[]} +{"id":1193,"hr_input":"WRK-803: 급여 문의, 이번 달 수당 반영 여부 확인 필요","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 문의, 이번 달 수당 반영 여부 확인 필요"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 문의, 이번 달 수당 반영"}]},"structural_issues":[]} +{"id":1197,"hr_input":"WRK-807 통장사본 받아서 급여계좌 재등록 부탁","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 재등록 부탁"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"통장사본 받아서"},{"intent":"PAYROLL_EXPLANATION","evidence":"받아서 급여계좌 재등록"}]},"structural_issues":[]} +{"id":1199,"hr_input":"WRK-809 휴가 처리 완료됨","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"휴가 처리 완료됨"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-809 휴가 처리"}]},"structural_issues":[]} +{"id":1202,"hr_input":"WRK-812 무단결근 3일째, 라인 대체인력 배치 및 급여 미지급 안내 필요","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 3일째"},{"intent":"WORK_INSTRUCTION","evidence":"라인 대체인력 배치"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 미지급 안내 필요"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-812 무단결근 3일째,"},{"intent":"WORK_INSTRUCTION","evidence":"라인 대체인력 배치 및"},{"intent":"PAYROLL_EXPLANATION","evidence":"및 급여 미지급 안내 필요"}]},"structural_issues":[]} +{"id":1207,"hr_input":"WRK-817: 오늘 라인 청소 및 정리 지시","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 청소 및 정리 지시"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 청소 및"}]},"structural_issues":[]} +{"id":1208,"hr_input":"WRK-818 연락두절, 급여 미지급 문제로 인한 잠적 가능성 확인 필요","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"연락두절"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 미지급 문제로 인한 잠적 가능성 확인 필요"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-818 연락두절,"}]},"structural_issues":[]} +{"id":1211,"hr_input":"WRK-821: 오늘 컨베이어 작업 배정, 초과근무 발생 시 사전 승인 필요","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 컨베이어 작업 배정, 초과근무 발생 시 사전 승인 필요"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"컨베이어 작업 배정,"},{"intent":"PAYROLL_EXPLANATION","evidence":"초과근무 발생 시"}]},"structural_issues":[]} +{"id":1221,"hr_input":"WRK-831 오늘 새 라인 투입, 첫 주 급여는 기본급만 지급 예정","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 라인 투입"},{"intent":"PAYROLL_EXPLANATION","evidence":"첫 주 급여는 기본급만 지급 예정"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 라인 투입, 첫 주"},{"intent":"PAYROLL_EXPLANATION","evidence":"첫 주 급여는 기본급만 지급 예정"}]},"structural_issues":[]} +{"id":1229,"hr_input":"WRK-839: 계약만료 임박, 여권과 계약서 받아서 재계약 서류 준비 및 라인 재배치 논의","expected":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료 임박"},{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서 받아서 재계약 서류 준비"},{"intent":"WORK_INSTRUCTION","evidence":"라인 재배치 논의"}]},"predicted":{"intents":[{"intent":"EXPIRY_RENEWAL","evidence":"계약만료 임박,"},{"intent":"DOCUMENT_REQUEST","evidence":"여권과 계약서 받아서 재계약"}]},"structural_issues":[]} +{"id":1240,"hr_input":"신규 WRK-850 여권, 계약서, 통장사본 받아서 등록, 보험가입, 급여계좌 개설 한번에 처리해주세요.","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권, 계약서, 통장사본 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록, 보험가입"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌 개설 한번에 처리"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권, 계약서, 통장사본 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록, 보험가입, 급여계좌 개설 한번에"},{"intent":"PAYROLL_EXPLANATION","evidence":"보험가입, 급여계좌 개설 한번에 처리해주세요."}]},"structural_issues":[]} +{"id":1241,"hr_input":"WRK-851 사업장 변경 확정, 새 근무지 작업지시는 새 회사가 전달 예정","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 확정"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-851 사업장 변경 확정,"},{"intent":"WORK_INSTRUCTION","evidence":"확정, 새 근무지 작업지시는 새"}]},"structural_issues":[]} +{"id":1244,"hr_input":"WRK-854 무단결근 이틀째, 고용허가기간 만료도 임박해서 신고와 연장 여부 동시 검토 필요","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근 이틀째"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료도 임박해서 신고와 연장 여부 동시 검토 필요"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-854 무단결근 이틀째,"},{"intent":"EXPIRY_RENEWAL","evidence":"고용허가기간 만료도 임박해서 신고와 연장 여부"}]},"structural_issues":[]} +{"id":1245,"hr_input":"WRK-855 계약서 받아서 갱신, 배치는 기존 라인 유지, 급여 인상 여부는 미정","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"갱신"},{"intent":"WORK_INSTRUCTION","evidence":"배치는 기존 라인 유지"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여 인상 여부는 미정"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"계약서 받아서"},{"intent":"EXPIRY_RENEWAL","evidence":"받아서 갱신,"}]},"structural_issues":[]} +{"id":1254,"hr_input":"WRK-864 오늘부터 라인 재배치, 이유는 사업장 이전임","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘부터 라인 재배치"},{"intent":"EMPLOYMENT_CHANGE","evidence":"이유는 사업장 이전임"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-864 오늘부터"},{"intent":"WORK_INSTRUCTION","evidence":"오늘부터 라인 재배치,"}]},"structural_issues":[]} +{"id":1257,"hr_input":"WRK-867: 오늘 작업지시 변경 없음","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업지시 변경 없음"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 작업지시 변경 없음"}]},"structural_issues":[]} +{"id":1259,"hr_input":"WRK-869 무단결근, 오늘 오후 연락 닿음","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근, 오늘 오후 연락 닿음"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-869 무단결근, 오늘"},{"intent":"WORK_INSTRUCTION","evidence":"무단결근, 오늘 오후 연락"}]},"structural_issues":[]} +{"id":1262,"hr_input":"WRK-872 사업장 변경 신청됨, 체류기간 연장도 함께 처리해야 함","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"사업장 변경 신청됨"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장도 함께 처리해야 함"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-872 사업장 변경 신청됨,"},{"intent":"EXPIRY_RENEWAL","evidence":"체류기간 연장도 함께"}]},"structural_issues":[]} +{"id":1264,"hr_input":"WRK-874 오늘 새 라인 배치, 초과근무 발생 시 수당 계산법 미리 안내","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 라인 배치"},{"intent":"PAYROLL_EXPLANATION","evidence":"초과근무 발생 시 수당 계산법 미리 안내"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 새 라인 배치,"},{"intent":"PAYROLL_EXPLANATION","evidence":"초과근무 발생 시 수당 계산법 미리"}]},"structural_issues":[]} +{"id":1265,"hr_input":"WRK-875 여권 받아서 재발급, 등록증도 함께 받아줘","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서 재발급, 등록증도 함께 받아줘"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서 재발급, 등록증도 함께 받아줘"}]},"structural_issues":[]} +{"id":1266,"hr_input":"WRK-876: 오늘 무단결근, 연락 시도 중","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 무단결근, 연락 시도 중"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-876: 오늘 무단결근, 연락 시도"},{"intent":"WORK_INSTRUCTION","evidence":"WRK-876: 오늘 무단결근,"}]},"structural_issues":[]} +{"id":1275,"hr_input":"신규 WRK-885 여권 받아서 등록 완료, 급여계좌는 다음 주 개설 예정","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록 완료"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여계좌는 다음 주 개설 예정"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 WRK-885 여권 받아서 등록 완료,"},{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"}]},"structural_issues":[]} +{"id":1277,"hr_input":"WRK-887: 급여 문의, 이번 달 야근수당 미반영 확인됨","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 문의, 이번 달 야근수당 미반영 확인됨"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 문의, 이번 달 야근수당 미반영"}]},"structural_issues":[]} +{"id":1282,"hr_input":"신규 WRK-892 등록증과 사진 받아서 등록 및 초기 배치 함께 진행","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증과 사진 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록"},{"intent":"WORK_INSTRUCTION","evidence":"초기 배치 함께 진행"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"등록증과 사진 받아서 등록"},{"intent":"WORKER_ONBOARDING","evidence":"받아서 등록 및 초기 배치"}]},"structural_issues":[]} +{"id":1289,"hr_input":"WRK-899: 급여 명세서 문의, 국민연금 및 건강보험 공제 설명 완료","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세서 문의, 국민연금 및 건강보험 공제 설명 완료"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 명세서 문의, 국민연금"}]},"structural_issues":[]} +{"id":1299,"hr_input":"WRK-909 오늘 무단결근, 라인 대체 배치함","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 무단결근"},{"intent":"WORK_INSTRUCTION","evidence":"라인 대체 배치함"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-909 오늘 무단결근,"},{"intent":"WORK_INSTRUCTION","evidence":"오늘 무단결근, 라인 대체 배치함"}]},"structural_issues":[]} +{"id":1304,"hr_input":"WRK-914: 오늘 휴가 처리, 급여는 무급 안내","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 휴가 처리"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여는 무급 안내"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"WRK-914: 오늘 휴가"},{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-914: 오늘 휴가 처리,"},{"intent":"PAYROLL_EXPLANATION","evidence":"급여는 무급 안내"}]},"structural_issues":[]} +{"id":1305,"hr_input":"신규 WRK-915 여권과 건강진단서 받아서 등록 처리 부탁","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 건강진단서 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록 처리 부탁"}]},"predicted":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권과 건강진단서 받아서 등록"},{"intent":"WORKER_ONBOARDING","evidence":"받아서 등록 처리 부탁"}]},"structural_issues":[]} +{"id":1311,"hr_input":"신규 WRK-921 여권 받아서 등록하고, 초기 배치는 검사라인으로 지시함","expected":{"intents":[{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORKER_ONBOARDING","evidence":"등록"},{"intent":"WORK_INSTRUCTION","evidence":"초기 배치는 검사라인으로 지시함"}]},"predicted":{"intents":[{"intent":"WORKER_ONBOARDING","evidence":"신규 WRK-921 여권 받아서 등록하고, 초기"},{"intent":"DOCUMENT_REQUEST","evidence":"여권 받아서"},{"intent":"WORK_INSTRUCTION","evidence":"초기 배치는 검사라인으로 지시함"}]},"structural_issues":[]} +{"id":1313,"hr_input":"WRK-923: 무단결근, 오늘 오전 대체인력 투입","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"무단결근"},{"intent":"WORK_INSTRUCTION","evidence":"오늘 오전 대체인력 투입"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-923: 무단결근, 오늘"},{"intent":"WORK_INSTRUCTION","evidence":"오늘 오전 대체인력 투입"}]},"structural_issues":[]} +{"id":1315,"hr_input":"WRK-925: 오늘 라인 배치 이상 없음, 특이 문의도 없음","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 이상 없음, 특이 문의도 없음"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 이상 없음,"}]},"structural_issues":[]} +{"id":1318,"hr_input":"WRK-928: 급여 지연 문의 답변 완료","expected":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 지연 문의 답변 완료"}]},"predicted":{"intents":[{"intent":"PAYROLL_EXPLANATION","evidence":"급여 지연 문의 답변 완료"}]},"structural_issues":[]} +{"id":1320,"hr_input":"WRK-930 오늘 무단결근으로 라인 공백, 급하게 대체인력 배치했고 신고도 접수함","expected":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"오늘 무단결근으로 라인 공백"},{"intent":"WORK_INSTRUCTION","evidence":"급하게 대체인력 배치했고"}]},"predicted":{"intents":[{"intent":"EMPLOYMENT_CHANGE","evidence":"WRK-930 오늘 무단결근으로 라인 공백,"},{"intent":"WORK_INSTRUCTION","evidence":"급하게 대체인력 배치했고"}]},"structural_issues":[]} +{"id":1327,"hr_input":"WRK-937 오늘 라인 배치 확인, 특이사항 없음","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 확인, 특이사항 없음"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 라인 배치 확인,"}]},"structural_issues":[]} +{"id":1332,"hr_input":"WRK-942: 오늘 안전교육 지시, 참석자 명단 확인 요청","expected":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 안전교육 지시, 참석자 명단 확인 요청"}]},"predicted":{"intents":[{"intent":"WORK_INSTRUCTION","evidence":"오늘 안전교육 지시, 참석자"}]},"structural_issues":[]} diff --git a/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml b/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml new file mode 100644 index 0000000..d0a9ef7 --- /dev/null +++ b/fowoco-knowledge/data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml @@ -0,0 +1,116 @@ +experiment_id: FOWOCO-INTENT-PROVISIONAL-CHAR-NGRAM-NB-V1 +version: 0.1.0 +status: provisional_pre_consensus +purpose: 수정 전 라벨로 모델링·평가 파이프라인과 오류 유형을 검증하는 하한선 +source: + dataset_id: FOWOCO-HR-INTENT-CANDIDATES + version: 1.1.0 + status: recheck_required + path: data/intent/hr_intent_dataset.jsonl + record_count: 1340 + sha256: 4f4ebfdd4170a78def33e31edbed8315921c0b67934f5ff8612595dcd479bed2 + manifest_path: data/intent/manifest.yaml +split: + manifest_path: data/intent/splits/provisional-v1/manifest.yaml + manifest_sha256: 3118f4aa8e4c4859efefed1cbde04ad7773ae0e77323ad9022b3fee9e0b2364b + status: provisional_pending_consensus + train: + path: data/intent/splits/provisional-v1/train_ids.txt + record_count: 1072 + sha256: 264011422e233e28ab7ab4b93579f634a6e53c00730d254b7ca2749d71575cb6 + validation: + path: data/intent/splits/provisional-v1/validation_ids.txt + record_count: 268 + sha256: 96da74e15519f9610ef316bc3e7499d06832c36564180607a3565b851bc8ba85 + validation_used_for_training_or_thresholds: false +model: + family: multilabel_multinomial_naive_bayes + features: character_ngrams + ngram_min: 2 + ngram_max: 5 + alpha: 1.0 + threshold_calibration: train_in_sample_f1_only + thresholds: + WORK_INSTRUCTION: 6.096038744112 + DOCUMENT_REQUEST: 37.285624039926 + PAYROLL_EXPLANATION: 20.199619612251 + WORKER_ONBOARDING: 35.842590492089 + EMPLOYMENT_CHANGE: 18.694991220673 + EXPIRY_RENEWAL: 25.444998825749 + OUT_OF_SCOPE: 40.111322163511 + evidence_strategy: highest_positive_weight_contiguous_token_span + evidence_max_tokens: 6 + external_dependencies: [] +metrics: + record_count: 268 + intent_exact_match: 0.854478 + macro_f1: 0.938066 + micro_f1: 0.940361 + per_intent: + WORK_INSTRUCTION: + precision: 0.876712 + recall: 0.914286 + f1: 0.895105 + support: 70 + DOCUMENT_REQUEST: + precision: 1.0 + recall: 0.971429 + f1: 0.985507 + support: 70 + PAYROLL_EXPLANATION: + precision: 0.942308 + recall: 0.875 + f1: 0.907407 + support: 56 + WORKER_ONBOARDING: + precision: 1.0 + recall: 0.923077 + f1: 0.96 + support: 39 + EMPLOYMENT_CHANGE: + precision: 0.947368 + recall: 0.964286 + f1: 0.955752 + support: 56 + EXPIRY_RENEWAL: + precision: 0.959184 + recall: 0.979167 + f1: 0.969072 + support: 48 + OUT_OF_SCOPE: + precision: 1.0 + recall: 0.807692 + f1: 0.893617 + support: 26 + evidence_gold_exact_match: 0.286136 + evidence_gold_item_count: 339 + structural_gate_rates: + JSON_SCHEMA_VALID: 1.0 + EVIDENCE_EXACT_SUBSTRING: 1.0 + INTENT_ORDER: 1.0 + OUT_OF_SCOPE_EXCLUSIVE: 1.0 + error_case_counts: + EVIDENCE_TOO_BROAD: 114 + EXTRA_INTENT: 17 + MISSING_INTENT: 24 +artifacts: + predictions: + path: data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl + record_count: 268 + sha256: 2b138faee3c91ab7fcae5e7fa7ed0fcc613138b07fc05fe1d65f0ecae063c108 +claims_not_allowed: +- 최종 모델 성능 +- Gold Test 성능 +- 운영 배포 가능 +- A/B consensus가 반영된 라벨 성능 +rerun_required_when: +- A/B consensus가 원본 라벨을 변경 +- source 또는 split checksum이 변경 +- Intent 규칙 또는 모델 설정이 변경 +limitations: +- Reviewer B와 A/B consensus 전 수정 전 라벨을 사용함 +- Train 내부 점수로 threshold를 정해 운영 threshold로 사용할 수 없음 +- 문자 n-gram Naive Bayes는 A.X 또는 Transformer 모델 후보를 대체하지 않음 +- evidence 추출은 규칙 기반 하한선이며 자연스러움이나 최소 완결 구간을 보장하지 않음 +- Validation은 baseline 개발용이며 독립 Gold Test가 아님 +- 모델 학습·추론 코드는 최종적으로 fowoco/ai 저장소로 이전해야 함 diff --git a/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md b/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md index 51c9afe..e025f1e 100644 --- a/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md +++ b/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md @@ -13,6 +13,7 @@ knowledge 저장소는 다음 자료를 제공한다. | 분할 manifest | `data/intent/splits/provisional-v1/manifest.yaml` | consensus 대기 | | 평가 정책 | `knowledge/intent_evaluation_policy.yaml` | baseline 전 임시 목표 | | Gold 계획 | `docs/INTENT_GOLD_TEST_PLAN.md` | 작성 전 | +| 임시 baseline 가이드 | `docs/INTENT_PROVISIONAL_BASELINE.md` | 수정 전 라벨 실험 | AI 저장소는 프롬프트, 모델 어댑터, 추론·평가 코드와 실험 결과를 관리한다. diff --git a/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md b/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md new file mode 100644 index 0000000..034a5b9 --- /dev/null +++ b/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md @@ -0,0 +1,66 @@ +# Intent 수정 전 라벨 Provisional Baseline + +## 목적 + +이 baseline은 A/B consensus 전 원본 1,340건으로 모델링 파이프라인이 정상적으로 +작동하는지 확인하는 하한선이다. 최종 모델 또는 Gold Test 성능을 주장하지 않는다. + +## 데이터 + +- Source: `data/intent/hr_intent_dataset.jsonl` +- 상태: `recheck_required` +- Train: 1,072건 +- Validation: 268건 +- Split: `data/intent/splits/provisional-v1/manifest.yaml` +- Reviewer A 제안: 원본에 미반영 +- Reviewer B·Consensus: 대기 + +Validation ID는 모델 학습과 threshold 산정에 사용하지 않는다. + +## 모델 + +외부 라이브러리와 API 없이 실행되는 문자 n-gram 다중라벨 Naive Bayes다. 각 +Intent를 one-vs-rest 방식으로 학습하고 Train 내부 점수만으로 임시 threshold를 +정한다. evidence는 해당 Intent에 양의 가중치를 갖는 연속 토큰 구간을 선택한다. + +이 모델은 데이터·평가 파이프라인 검증용이며 A.X, BERT 또는 Transformer 후보를 +대체하지 않는다. + +## 저장소 경계 + +AI 저장소의 작업 충돌을 피하기 위해 현재는 knowledge 저장소의 `model` 브랜치에서 +임시로 실행한다. 이 브랜치의 모델 학습·추론 코드는 knowledge `main` 병합 대상이 +아니며, 실험 구조가 확인되면 `fowoco/ai` 저장소로 이전한다. knowledge 저장소에는 +최종적으로 데이터 계약, split, 평가 정책과 검증 리포트만 남긴다. + +## 실행 + +```bash +python -m fowoco_knowledge run-intent-provisional-baseline +``` + +생성 파일: + +- `data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml` +- `data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl` + +Report에는 source·split checksum, 모델 설정, Intent 지표, evidence 일치율, 구조적 +Gate, 오류 유형과 금지 주장을 기록한다. 생성 시각은 넣지 않아 같은 입력과 코드에서 +동일한 결과를 재생성할 수 있다. + +## 결과 해석 + +- `intent_exact_match`, `macro_f1`, `micro_f1`: 수정 전 라벨과의 일치 정도 +- `evidence_gold_exact_match`: 기존 evidence와 완전히 같은 구간을 예측한 비율 +- `structural_gate_rates`: JSON Schema, exact substring, 순서, OUT_OF_SCOPE 단독성 +- `error_case_counts`: Intent 누락·추가와 evidence 과다 범위 등의 오류 건수 + +Validation은 개발 중 확인용이므로 모델 선택을 반복하면 Validation에도 과적합될 수 +있다. 최종 비교는 별도로 잠근 Gold Test 240건이 확보된 뒤 수행한다. + +## Consensus 반영 후 + +1. 원본 JSONL과 `data/intent/manifest.yaml` version·checksum을 갱신한다. +2. PR #33의 split 생성기를 같은 seed로 다시 실행한다. +3. 이 baseline을 다시 실행한다. +4. 수정 전·후 결과를 다른 데이터 version으로 보관하고 직접 혼합하지 않는다. diff --git a/fowoco-knowledge/knowledge/manifest.yaml b/fowoco-knowledge/knowledge/manifest.yaml index 614a8de..2abe08b 100644 --- a/fowoco-knowledge/knowledge/manifest.yaml +++ b/fowoco-knowledge/knowledge/manifest.yaml @@ -41,6 +41,8 @@ datasets: intent_split_manifest: data/intent/splits/provisional-v1/manifest.yaml intent_train_ids: data/intent/splits/provisional-v1/train_ids.txt intent_validation_ids: data/intent/splits/provisional-v1/validation_ids.txt + intent_provisional_baseline_report: data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml + intent_provisional_baseline_predictions: data/experiments/intent/provisional-char-ngram-nb-v1/predictions.jsonl review_policy: minimum_reviewers_for_official_knowledge: 2 all_outbound_messages_require_hr_approval: true diff --git a/fowoco-knowledge/src/fowoco_knowledge/cli.py b/fowoco-knowledge/src/fowoco_knowledge/cli.py index db3a84a..8d8c8fb 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/cli.py +++ b/fowoco-knowledge/src/fowoco_knowledge/cli.py @@ -12,6 +12,7 @@ DEFAULT_VALIDATION_RATIO, build_and_write_intent_split, ) +from .provisional_baseline import run_provisional_baseline from .repository import KnowledgeRepository from .validation import KnowledgeValidator @@ -83,6 +84,16 @@ def build_parser() -> argparse.ArgumentParser: type=float, default=DEFAULT_VALIDATION_RATIO, ) + + baseline_parser = subparsers.add_parser( + "run-intent-provisional-baseline", + help="Run a dependency-free provisional Intent baseline", + ) + baseline_parser.add_argument( + "--output-dir", + type=Path, + help="Output directory inside the knowledge project", + ) return parser @@ -161,4 +172,12 @@ def main(argv: Sequence[str] | None = None) -> int: print(json.dumps(manifest, ensure_ascii=False, indent=2)) return 0 + if args.command == "run-intent-provisional-baseline": + report = run_provisional_baseline( + repository.root, + output_dir=args.output_dir, + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + return 0 + return 2 diff --git a/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py b/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py new file mode 100644 index 0000000..cd398ed --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py @@ -0,0 +1,587 @@ +from __future__ import annotations + +import json +import math +import re +from collections import Counter +from pathlib import Path +from typing import Any + +import yaml + +from .ingestion import file_sha256 +from .intent_model import validate_intent_model_output +from .intent_split import load_intent_cases, read_split_ids + +INTENT_CODES = ( + "WORK_INSTRUCTION", + "DOCUMENT_REQUEST", + "PAYROLL_EXPLANATION", + "WORKER_ONBOARDING", + "EMPLOYMENT_CHANGE", + "EXPIRY_RENEWAL", + "OUT_OF_SCOPE", +) +WORKER_ID_PATTERN = re.compile(r"(?i)\bwrk[-_ ]?\d+\b") +TOKEN_PATTERN = re.compile(r"\S+") +DEFAULT_OUTPUT_DIR = Path("data/experiments/intent/provisional-char-ngram-nb-v1") + + +class ProvisionalBaselineError(ValueError): + """Raised when the provisional baseline cannot run reproducibly.""" + + +def compact_json(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + + +def normalize_text(text: str) -> str: + normalized = WORKER_ID_PATTERN.sub("", text) + return " ".join(normalized.casefold().split()) + + +def char_ngrams(text: str, ngram_min: int, ngram_max: int) -> Counter[str]: + normalized = normalize_text(text) + features: Counter[str] = Counter() + for ngram_size in range(ngram_min, ngram_max + 1): + for start in range(max(0, len(normalized) - ngram_size + 1)): + feature = normalized[start : start + ngram_size] + if feature.strip(): + features[feature] += 1 + return features + + +def _feature_log_odds( + positive_texts: list[str], + negative_texts: list[str], + *, + ngram_min: int, + ngram_max: int, + alpha: float, +) -> tuple[float, dict[str, float]]: + positive_counts: Counter[str] = Counter() + negative_counts: Counter[str] = Counter() + for text in positive_texts: + positive_counts.update(char_ngrams(text, ngram_min, ngram_max)) + for text in negative_texts: + negative_counts.update(char_ngrams(text, ngram_min, ngram_max)) + + vocabulary = set(positive_counts) | set(negative_counts) + vocabulary_size = max(1, len(vocabulary)) + positive_total = sum(positive_counts.values()) + negative_total = sum(negative_counts.values()) + positive_denominator = positive_total + alpha * vocabulary_size + negative_denominator = negative_total + alpha * vocabulary_size + weights = { + feature: math.log((positive_counts[feature] + alpha) / positive_denominator) + - math.log((negative_counts[feature] + alpha) / negative_denominator) + for feature in vocabulary + } + prior = math.log((len(positive_texts) + alpha) / (len(negative_texts) + alpha)) + return prior, weights + + +def _best_training_threshold(scores: list[float], truths: list[bool]) -> float: + if len(scores) != len(truths) or not scores: + raise ProvisionalBaselineError("threshold calibration inputs are invalid") + positive_total = sum(truths) + if positive_total == 0: + return math.inf + + grouped: dict[float, list[bool]] = {} + for score, truth in zip(scores, truths, strict=True): + grouped.setdefault(score, []).append(truth) + + true_positives = 0 + false_positives = 0 + best_f1 = -1.0 + best_threshold = math.inf + for threshold in sorted(grouped, reverse=True): + group = grouped[threshold] + true_positives += sum(group) + false_positives += len(group) - sum(group) + false_negatives = positive_total - true_positives + denominator = 2 * true_positives + false_positives + false_negatives + f1 = 2 * true_positives / denominator if denominator else 0.0 + if f1 > best_f1: + best_f1 = f1 + best_threshold = threshold + return best_threshold + + +class CharacterNgramMultilabelNB: + """Dependency-free provisional lower-bound model for pipeline verification.""" + + def __init__( + self, + *, + ngram_min: int = 2, + ngram_max: int = 5, + alpha: float = 1.0, + evidence_max_tokens: int = 6, + ) -> None: + if ngram_min < 1 or ngram_max < ngram_min: + raise ValueError("invalid n-gram range") + self.ngram_min = ngram_min + self.ngram_max = ngram_max + self.alpha = alpha + self.evidence_max_tokens = evidence_max_tokens + self.priors: dict[str, float] = {} + self.weights: dict[str, dict[str, float]] = {} + self.evidence_weights: dict[str, dict[str, float]] = {} + self.thresholds: dict[str, float] = {} + + def _score(self, text: str, intent: str) -> float: + features = char_ngrams(text, self.ngram_min, self.ngram_max) + weights = self.weights[intent] + return self.priors[intent] + sum( + count * weights.get(feature, 0.0) for feature, count in features.items() + ) + + def fit(self, cases: list[dict[str, Any]]) -> CharacterNgramMultilabelNB: + if not cases: + raise ProvisionalBaselineError("training cases are empty") + labels_by_case = [{item["intent"] for item in case["intents"]} for case in cases] + for intent in INTENT_CODES: + positive_texts = [ + case["hr_input"] + for case, labels in zip(cases, labels_by_case, strict=True) + if intent in labels + ] + negative_texts = [ + case["hr_input"] + for case, labels in zip(cases, labels_by_case, strict=True) + if intent not in labels + ] + if not positive_texts or not negative_texts: + raise ProvisionalBaselineError( + f"{intent}: positive/negative training cases required" + ) + prior, weights = _feature_log_odds( + positive_texts, + negative_texts, + ngram_min=self.ngram_min, + ngram_max=self.ngram_max, + alpha=self.alpha, + ) + self.priors[intent] = prior + self.weights[intent] = weights + + positive_evidence = [ + item["evidence"] + for case in cases + for item in case["intents"] + if item["intent"] == intent and isinstance(item["evidence"], str) + ] + negative_evidence = [ + item["evidence"] + for case in cases + for item in case["intents"] + if item["intent"] != intent and isinstance(item["evidence"], str) + ] + if positive_evidence and negative_evidence: + _, evidence_weights = _feature_log_odds( + positive_evidence, + negative_evidence, + ngram_min=self.ngram_min, + ngram_max=self.ngram_max, + alpha=self.alpha, + ) + self.evidence_weights[intent] = evidence_weights + else: + self.evidence_weights[intent] = weights + + for intent in INTENT_CODES: + scores = [self._score(case["hr_input"], intent) for case in cases] + truths = [intent in labels for labels in labels_by_case] + self.thresholds[intent] = _best_training_threshold(scores, truths) + return self + + def _extract_evidence(self, hr_input: str, intent: str) -> str: + tokens = list(TOKEN_PATTERN.finditer(hr_input)) + if not tokens: + return hr_input + weights = self.evidence_weights[intent] + best: tuple[float, int, int, str] | None = None + for start_index in range(len(tokens)): + for end_index in range( + start_index, + min(len(tokens), start_index + self.evidence_max_tokens), + ): + start = tokens[start_index].start() + end = tokens[end_index].end() + candidate = hr_input[start:end] + if WORKER_ID_PATTERN.fullmatch(candidate): + continue + feature_score = sum( + count * max(0.0, weights.get(feature, 0.0)) + for feature, count in char_ngrams( + candidate, + self.ngram_min, + self.ngram_max, + ).items() + ) + normalized_score = feature_score / math.sqrt(max(1, len(candidate))) + rank = (normalized_score, -len(candidate), -start, candidate) + if best is None or rank > best: + best = rank + if best is None or best[0] <= 0: + return hr_input + return best[3] + + def predict(self, hr_input: str) -> dict[str, list[dict[str, str | None]]]: + if not self.thresholds: + raise ProvisionalBaselineError("model must be fitted before prediction") + scores = {intent: self._score(hr_input, intent) for intent in INTENT_CODES} + selected = [intent for intent in INTENT_CODES if scores[intent] >= self.thresholds[intent]] + if not selected: + selected = [max(INTENT_CODES, key=scores.__getitem__)] + + if "OUT_OF_SCOPE" in selected: + other_selected = [intent for intent in selected if intent != "OUT_OF_SCOPE"] + if not other_selected or scores["OUT_OF_SCOPE"] >= max( + scores[intent] for intent in other_selected + ): + return {"intents": [{"intent": "OUT_OF_SCOPE", "evidence": None}]} + selected = other_selected + + evidence_items = [ + { + "intent": intent, + "evidence": self._extract_evidence(hr_input, intent), + "_score": scores[intent], + } + for intent in selected + ] + evidence_items.sort( + key=lambda item: ( + hr_input.find(str(item["evidence"])), + -float(item["_score"]), + ) + ) + return { + "intents": [ + { + "intent": str(item["intent"]), + "evidence": str(item["evidence"]), + } + for item in evidence_items[:6] + ] + } + + def configuration(self) -> dict[str, Any]: + return { + "family": "multilabel_multinomial_naive_bayes", + "features": "character_ngrams", + "ngram_min": self.ngram_min, + "ngram_max": self.ngram_max, + "alpha": self.alpha, + "threshold_calibration": "train_in_sample_f1_only", + "thresholds": {intent: round(self.thresholds[intent], 12) for intent in INTENT_CODES}, + "evidence_strategy": ("highest_positive_weight_contiguous_token_span"), + "evidence_max_tokens": self.evidence_max_tokens, + "external_dependencies": [], + } + + +def _safe_divide(numerator: int, denominator: int) -> float: + return numerator / denominator if denominator else 0.0 + + +def _metric_counts( + expected: set[str], + predicted: set[str], + counts: dict[str, Counter[str]], +) -> None: + for intent in INTENT_CODES: + if intent in expected and intent in predicted: + counts[intent]["tp"] += 1 + elif intent not in expected and intent in predicted: + counts[intent]["fp"] += 1 + elif intent in expected and intent not in predicted: + counts[intent]["fn"] += 1 + + +def _metrics_from_counts( + counts: dict[str, Counter[str]], +) -> tuple[dict[str, Any], float, float]: + per_intent: dict[str, Any] = {} + totals: Counter[str] = Counter() + f1_values: list[float] = [] + for intent in INTENT_CODES: + intent_counts = counts[intent] + totals.update(intent_counts) + precision = _safe_divide( + intent_counts["tp"], + intent_counts["tp"] + intent_counts["fp"], + ) + recall = _safe_divide( + intent_counts["tp"], + intent_counts["tp"] + intent_counts["fn"], + ) + f1 = _safe_divide(2 * precision * recall, precision + recall) + f1_values.append(f1) + per_intent[intent] = { + "precision": round(precision, 6), + "recall": round(recall, 6), + "f1": round(f1, 6), + "support": intent_counts["tp"] + intent_counts["fn"], + } + micro_precision = _safe_divide( + totals["tp"], + totals["tp"] + totals["fp"], + ) + micro_recall = _safe_divide( + totals["tp"], + totals["tp"] + totals["fn"], + ) + micro_f1 = _safe_divide( + 2 * micro_precision * micro_recall, + micro_precision + micro_recall, + ) + return per_intent, sum(f1_values) / len(f1_values), micro_f1 + + +def evaluate_baseline( + *, + model: CharacterNgramMultilabelNB, + validation_cases: list[dict[str, Any]], + output_schema: dict[str, Any], +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + label_counts = {intent: Counter() for intent in INTENT_CODES} + structural_failures: Counter[str] = Counter() + error_counts: Counter[str] = Counter() + predictions: list[dict[str, Any]] = [] + intent_exact_matches = 0 + evidence_exact_matches = 0 + evidence_expected_count = 0 + + for case in validation_cases: + output = model.predict(case["hr_input"]) + issues = validate_intent_model_output( + case["hr_input"], + output, + output_schema, + ) + issue_codes = sorted({issue.code for issue in issues}) + structural_failures.update(issue_codes) + + expected_codes = [item["intent"] for item in case["intents"]] + predicted_codes = [item["intent"] for item in output["intents"]] + expected_set = set(expected_codes) + predicted_set = set(predicted_codes) + _metric_counts(expected_set, predicted_set, label_counts) + if expected_codes == predicted_codes: + intent_exact_matches += 1 + if expected_set - predicted_set: + error_counts["MISSING_INTENT"] += 1 + if predicted_set - expected_set: + error_counts["EXTRA_INTENT"] += 1 + + expected_evidence = { + item["intent"]: item["evidence"] + for item in case["intents"] + if item["intent"] != "OUT_OF_SCOPE" + } + predicted_evidence = { + item["intent"]: item["evidence"] + for item in output["intents"] + if item["intent"] != "OUT_OF_SCOPE" + } + for intent, evidence in expected_evidence.items(): + evidence_expected_count += 1 + if predicted_evidence.get(intent) == evidence: + evidence_exact_matches += 1 + elif ( + isinstance(predicted_evidence.get(intent), str) + and isinstance(evidence, str) + and evidence in predicted_evidence[intent] + ): + error_counts["EVIDENCE_TOO_BROAD"] += 1 + + predictions.append( + { + "id": case["id"], + "hr_input": case["hr_input"], + "expected": {"intents": case["intents"]}, + "predicted": output, + "structural_issues": issue_codes, + } + ) + + per_intent, macro_f1, micro_f1 = _metrics_from_counts(label_counts) + record_count = len(validation_cases) + metrics = { + "record_count": record_count, + "intent_exact_match": round( + _safe_divide(intent_exact_matches, record_count), + 6, + ), + "macro_f1": round(macro_f1, 6), + "micro_f1": round(micro_f1, 6), + "per_intent": per_intent, + "evidence_gold_exact_match": round( + _safe_divide(evidence_exact_matches, evidence_expected_count), + 6, + ), + "evidence_gold_item_count": evidence_expected_count, + "structural_gate_rates": { + "JSON_SCHEMA_VALID": round( + 1 + - _safe_divide( + structural_failures["INVALID_JSON"], + record_count, + ), + 6, + ), + "EVIDENCE_EXACT_SUBSTRING": round( + 1 + - _safe_divide( + structural_failures["EVIDENCE_NOT_SUBSTRING"], + record_count, + ), + 6, + ), + "INTENT_ORDER": round( + 1 + - _safe_divide( + structural_failures["INTENT_ORDER_ERROR"], + record_count, + ), + 6, + ), + "OUT_OF_SCOPE_EXCLUSIVE": round( + 1 + - _safe_divide( + structural_failures["OUT_OF_SCOPE_MIXED"], + record_count, + ), + 6, + ), + }, + "error_case_counts": dict(sorted((error_counts + structural_failures).items())), + } + return metrics, predictions + + +def _resolve_inside_root(path: Path, root: Path) -> Path: + resolved = path.resolve() if path.is_absolute() else (root / path).resolve() + try: + resolved.relative_to(root.resolve()) + except ValueError as error: + raise ProvisionalBaselineError( + "baseline outputs must stay inside the knowledge project" + ) from error + return resolved + + +def run_provisional_baseline( + root: Path, + *, + output_dir: Path | None = None, +) -> dict[str, Any]: + root = root.resolve() + split_manifest_path = root / "data/intent/splits/provisional-v1/manifest.yaml" + split_manifest = yaml.safe_load(split_manifest_path.read_text(encoding="utf-8")) + if split_manifest["status"] == "locked": + raise ProvisionalBaselineError("this command is only for a provisional split") + + source_path = root / split_manifest["source"]["path"] + if file_sha256(source_path) != split_manifest["source"]["sha256"]: + raise ProvisionalBaselineError("source checksum differs from the split manifest") + train_path = root / split_manifest["outputs"]["train"]["path"] + validation_path = root / split_manifest["outputs"]["validation"]["path"] + for name, path in (("train", train_path), ("validation", validation_path)): + if file_sha256(path) != split_manifest["outputs"][name]["sha256"]: + raise ProvisionalBaselineError(f"{name} ID checksum differs from the split manifest") + + cases = load_intent_cases(source_path) + cases_by_id = {case["id"]: case for case in cases} + train_ids = read_split_ids(train_path) + validation_ids = read_split_ids(validation_path) + if set(train_ids) & set(validation_ids): + raise ProvisionalBaselineError("Train/Validation IDs overlap") + if set(train_ids) | set(validation_ids) != set(cases_by_id): + raise ProvisionalBaselineError("Train/Validation IDs do not cover the source") + + train_cases = [cases_by_id[case_id] for case_id in train_ids] + validation_cases = [cases_by_id[case_id] for case_id in validation_ids] + model = CharacterNgramMultilabelNB().fit(train_cases) + output_schema_path = root / "schemas/intent-model-output.schema.json" + output_schema = json.loads(output_schema_path.read_text(encoding="utf-8")) + metrics, predictions = evaluate_baseline( + model=model, + validation_cases=validation_cases, + output_schema=output_schema, + ) + + resolved_output_dir = _resolve_inside_root( + output_dir or DEFAULT_OUTPUT_DIR, + root, + ) + resolved_output_dir.mkdir(parents=True, exist_ok=True) + predictions_path = resolved_output_dir / "predictions.jsonl" + report_path = resolved_output_dir / "report.yaml" + with predictions_path.open("w", encoding="utf-8") as handle: + for prediction in predictions: + handle.write(compact_json(prediction) + "\n") + + report = { + "experiment_id": "FOWOCO-INTENT-PROVISIONAL-CHAR-NGRAM-NB-V1", + "version": "0.1.0", + "status": "provisional_pre_consensus", + "purpose": ("수정 전 라벨로 모델링·평가 파이프라인과 오류 유형을 검증하는 하한선"), + "source": { + **split_manifest["source"], + "manifest_path": "data/intent/manifest.yaml", + }, + "split": { + "manifest_path": str(split_manifest_path.relative_to(root)), + "manifest_sha256": file_sha256(split_manifest_path), + "status": split_manifest["status"], + "train": { + "path": str(train_path.relative_to(root)), + "record_count": len(train_cases), + "sha256": file_sha256(train_path), + }, + "validation": { + "path": str(validation_path.relative_to(root)), + "record_count": len(validation_cases), + "sha256": file_sha256(validation_path), + }, + "validation_used_for_training_or_thresholds": False, + }, + "model": model.configuration(), + "metrics": metrics, + "artifacts": { + "predictions": { + "path": str(predictions_path.relative_to(root)), + "record_count": len(predictions), + "sha256": file_sha256(predictions_path), + } + }, + "claims_not_allowed": [ + "최종 모델 성능", + "Gold Test 성능", + "운영 배포 가능", + "A/B consensus가 반영된 라벨 성능", + ], + "rerun_required_when": [ + "A/B consensus가 원본 라벨을 변경", + "source 또는 split checksum이 변경", + "Intent 규칙 또는 모델 설정이 변경", + ], + "limitations": [ + "Reviewer B와 A/B consensus 전 수정 전 라벨을 사용함", + "Train 내부 점수로 threshold를 정해 운영 threshold로 사용할 수 없음", + "문자 n-gram Naive Bayes는 A.X 또는 Transformer 모델 후보를 대체하지 않음", + "evidence 추출은 규칙 기반 하한선이며 자연스러움이나 최소 완결 구간을 보장하지 않음", + "Validation은 baseline 개발용이며 독립 Gold Test가 아님", + "모델 학습·추론 코드는 최종적으로 fowoco/ai 저장소로 이전해야 함", + ], + } + report_path.write_text( + yaml.safe_dump(report, allow_unicode=True, sort_keys=False), + encoding="utf-8", + ) + return report diff --git a/fowoco-knowledge/src/fowoco_knowledge/validation.py b/fowoco-knowledge/src/fowoco_knowledge/validation.py index b463275..3914a4c 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/validation.py +++ b/fowoco-knowledge/src/fowoco_knowledge/validation.py @@ -10,6 +10,7 @@ from jsonschema.exceptions import SchemaError from .ingestion import file_sha256 +from .intent_model import validate_intent_model_output from .intent_split import ( IntentSplitError, build_intent_split, @@ -96,6 +97,7 @@ def validate_all(self) -> list[str]: self._validate_intent_data() self._validate_intent_model_contract() self._validate_intent_split_data() + self._validate_intent_provisional_baseline() return self.errors def _validate_manifest_files(self) -> None: @@ -567,6 +569,98 @@ def _validate_intent_split_data(self) -> None: if expected_label_counts != split_manifest["statistics"]["label_counts"]: self.errors.append("intent split: label count statistics mismatch") + def _validate_intent_provisional_baseline(self) -> None: + manifest = self.repository.manifest + report_path = ( + self.repository.root / manifest["datasets"]["intent_provisional_baseline_report"] + ) + predictions_path = ( + self.repository.root / manifest["datasets"]["intent_provisional_baseline_predictions"] + ) + if not report_path.is_file() or not predictions_path.is_file(): + return + + report = self.repository.load_yaml( + manifest["datasets"]["intent_provisional_baseline_report"] + ) + intent_manifest = self.repository.load_yaml("data/intent/manifest.yaml") + split_manifest = self.repository.load_yaml(intent_manifest["split_manifest"]) + if report["status"] != "provisional_pre_consensus": + self.errors.append("intent baseline: status must remain provisional") + if report["source"]["status"] != "recheck_required": + self.errors.append("intent baseline: source status must show recheck_required") + for field in ("path", "version", "record_count", "sha256"): + if report["source"][field] != intent_manifest[field]: + self.errors.append(f"intent baseline: source {field} differs from intent manifest") + if report["split"]["manifest_sha256"] != file_sha256( + self.repository.root / intent_manifest["split_manifest"] + ): + self.errors.append("intent baseline: split manifest checksum mismatch") + if report["split"]["validation_used_for_training_or_thresholds"] is not False: + self.errors.append("intent baseline: Validation leakage flag must be false") + + predictions_artifact = report["artifacts"]["predictions"] + if file_sha256(predictions_path) != predictions_artifact["sha256"]: + self.errors.append("intent baseline: predictions checksum mismatch") + prediction_rows = [ + json.loads(line) + for line in predictions_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + if len(prediction_rows) != predictions_artifact["record_count"]: + self.errors.append("intent baseline: predictions record count mismatch") + if len(prediction_rows) != report["metrics"]["record_count"]: + self.errors.append("intent baseline: metric record count mismatch") + + source_cases = { + case["id"]: case + for case in load_intent_cases(self.repository.root / intent_manifest["path"]) + } + validation_ids = read_split_ids( + self.repository.root / split_manifest["outputs"]["validation"]["path"] + ) + prediction_ids = [row["id"] for row in prediction_rows] + if prediction_ids != list(validation_ids): + self.errors.append("intent baseline: predictions do not match ordered Validation IDs") + + output_schema = self.repository.load_json(manifest["schemas"]["intent_model_output"]) + for line_number, prediction in enumerate(prediction_rows, start=1): + source_case = source_cases.get(prediction["id"]) + if source_case is None: + self.errors.append(f"intent baseline line {line_number}: unknown source ID") + continue + if prediction["hr_input"] != source_case["hr_input"]: + self.errors.append( + f"intent baseline line {line_number}: hr_input differs from source" + ) + if prediction["expected"]["intents"] != source_case["intents"]: + self.errors.append( + f"intent baseline line {line_number}: expected intents differ from source" + ) + expected_issue_codes = sorted( + { + issue.code + for issue in validate_intent_model_output( + prediction["hr_input"], + prediction["predicted"], + output_schema, + ) + } + ) + if prediction["structural_issues"] != expected_issue_codes: + self.errors.append( + f"intent baseline line {line_number}: structural issues mismatch" + ) + + required_claim_blocks = { + "최종 모델 성능", + "Gold Test 성능", + "운영 배포 가능", + "A/B consensus가 반영된 라벨 성능", + } + if set(report["claims_not_allowed"]) != required_claim_blocks: + self.errors.append("intent baseline: required claim blocks differ") + def _index_unique(self, items: list[dict[str, Any]], kind: str) -> dict[str, dict[str, Any]]: indexed: dict[str, dict[str, Any]] = {} for item in items: diff --git a/fowoco-knowledge/tests/test_provisional_baseline.py b/fowoco-knowledge/tests/test_provisional_baseline.py new file mode 100644 index 0000000..105be80 --- /dev/null +++ b/fowoco-knowledge/tests/test_provisional_baseline.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + +from fowoco_knowledge.ingestion import file_sha256 +from fowoco_knowledge.intent_split import load_intent_cases, read_split_ids +from fowoco_knowledge.provisional_baseline import ( + CharacterNgramMultilabelNB, + evaluate_baseline, + run_provisional_baseline, +) +from fowoco_knowledge.repository import KnowledgeRepository + +ROOT = Path(__file__).resolve().parents[1] +REPORT_PATH = ROOT / "data/experiments/intent/provisional-char-ngram-nb-v1/report.yaml" + + +def test_provisional_baseline_report_is_reproducible() -> None: + committed = yaml.safe_load(REPORT_PATH.read_text(encoding="utf-8")) + generated_dir = ROOT / "data/experiments/intent/.test-provisional-baseline" + regenerated = run_provisional_baseline( + ROOT, + output_dir=Path("data/experiments/intent/.test-provisional-baseline"), + ) + try: + assert regenerated["source"] == committed["source"] + assert regenerated["split"] == committed["split"] + assert regenerated["model"] == committed["model"] + assert regenerated["metrics"] == committed["metrics"] + assert ( + file_sha256(generated_dir / "predictions.jsonl") + == committed["artifacts"]["predictions"]["sha256"] + ) + finally: + for path in generated_dir.iterdir(): + path.unlink() + generated_dir.rmdir() + + +def test_validation_records_are_not_used_for_fit() -> None: + split_manifest = yaml.safe_load( + (ROOT / "data/intent/splits/provisional-v1/manifest.yaml").read_text(encoding="utf-8") + ) + cases = load_intent_cases(ROOT / split_manifest["source"]["path"]) + cases_by_id = {case["id"]: case for case in cases} + train_ids = read_split_ids(ROOT / split_manifest["outputs"]["train"]["path"]) + validation_ids = read_split_ids(ROOT / split_manifest["outputs"]["validation"]["path"]) + + model = CharacterNgramMultilabelNB().fit([cases_by_id[case_id] for case_id in train_ids]) + assert set(train_ids).isdisjoint(validation_ids) + assert len(train_ids) == 1072 + assert len(validation_ids) == 268 + assert model.thresholds + + +def test_baseline_outputs_follow_structural_gates() -> None: + repository = KnowledgeRepository(ROOT) + output_schema = repository.load_json("schemas/intent-model-output.schema.json") + training_cases = [ + { + "id": 1, + "hr_input": "WRK-001 여권 사본 받아주세요", + "intents": [ + { + "intent": "DOCUMENT_REQUEST", + "evidence": "여권 사본 받아주세요", + } + ], + }, + { + "id": 2, + "hr_input": "WRK-002 비자 만료라 연장 준비", + "intents": [ + { + "intent": "EXPIRY_RENEWAL", + "evidence": "비자 만료라 연장 준비", + } + ], + }, + { + "id": 3, + "hr_input": "WRK-003 급여 공제 설명", + "intents": [ + { + "intent": "PAYROLL_EXPLANATION", + "evidence": "급여 공제 설명", + } + ], + }, + { + "id": 4, + "hr_input": "WRK-004 신규 근로자 등록", + "intents": [ + { + "intent": "WORKER_ONBOARDING", + "evidence": "신규 근로자 등록", + } + ], + }, + { + "id": 5, + "hr_input": "WRK-005 퇴사 처리", + "intents": [{"intent": "EMPLOYMENT_CHANGE", "evidence": "퇴사 처리"}], + }, + { + "id": 6, + "hr_input": "WRK-006 작업 배치 안내", + "intents": [{"intent": "WORK_INSTRUCTION", "evidence": "작업 배치 안내"}], + }, + { + "id": 7, + "hr_input": "오늘 점심 추천", + "intents": [{"intent": "OUT_OF_SCOPE", "evidence": None}], + }, + ] + model = CharacterNgramMultilabelNB( + ngram_min=1, + ngram_max=2, + ).fit(training_cases) + metrics, predictions = evaluate_baseline( + model=model, + validation_cases=training_cases, + output_schema=output_schema, + ) + + assert len(predictions) == 7 + assert metrics["structural_gate_rates"] == { + "JSON_SCHEMA_VALID": 1.0, + "EVIDENCE_EXACT_SUBSTRING": 1.0, + "INTENT_ORDER": 1.0, + "OUT_OF_SCOPE_EXCLUSIVE": 1.0, + } + assert all(not prediction["structural_issues"] for prediction in predictions) From 05e119dd1d58ee42d4722629c8bd6bd1147218cc Mon Sep 17 00:00:00 2001 From: hywznn Date: Mon, 27 Jul 2026 16:40:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?experiment(intent):=20A.X=20zero-shot=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8F=84=EA=B5=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fowoco-knowledge/docs/INTENT_AX_TESTING.md | 161 +++++ .../docs/INTENT_MODELING_HANDOFF.md | 1 + .../docs/INTENT_PROVISIONAL_BASELINE.md | 3 + .../intent/prompts/ax-zero-shot-v1.md | 65 ++ fowoco-knowledge/pyproject.toml | 6 + .../src/fowoco_knowledge/ax_intent.py | 636 ++++++++++++++++++ fowoco-knowledge/src/fowoco_knowledge/cli.py | 169 +++++ .../fowoco_knowledge/provisional_baseline.py | 38 +- fowoco-knowledge/tests/test_ax_intent.py | 295 ++++++++ 9 files changed, 1368 insertions(+), 6 deletions(-) create mode 100644 fowoco-knowledge/docs/INTENT_AX_TESTING.md create mode 100644 fowoco-knowledge/experiments/intent/prompts/ax-zero-shot-v1.md create mode 100644 fowoco-knowledge/src/fowoco_knowledge/ax_intent.py create mode 100644 fowoco-knowledge/tests/test_ax_intent.py diff --git a/fowoco-knowledge/docs/INTENT_AX_TESTING.md b/fowoco-knowledge/docs/INTENT_AX_TESTING.md new file mode 100644 index 0000000..22ebfc9 --- /dev/null +++ b/fowoco-knowledge/docs/INTENT_AX_TESTING.md @@ -0,0 +1,161 @@ +# A.X Intent 분류 테스트 가이드 + +## 1. 목적 + +Intent 규칙 v1.1 프롬프트를 A.X에 보내 `Intent + evidence` 출력과 오류 유형을 +확인한다. 현재 원본 라벨과 Validation은 `recheck_required` 상태이므로 결과를 최종 +모델 또는 Gold Test 성능으로 주장하지 않는다. + +공식 자료: + +- [skt/A.X-4.0-Light 모델 카드](https://huggingface.co/skt/A.X-4.0-Light) +- [SKT A.X 4.0 API 문서](https://github.com/SKT-AI/A.X-4.0/blob/main/apis/README.md) + +공식 모델 카드는 A.X 4.0 Light가 7B 모델이며 Transformers와 vLLM 실행 방법을 +제공한다. 무료 guest API 문서는 모델명을 `ax4`로만 안내하므로, 해당 API 결과를 +A.X 4.0 Light 결과라고 단정하지 않는다. + +## 2. 현재 공식 guest API 상태 + +2026-07-27 확인 결과, 공식 API 문서에 기재된 +`https://guest-api.sktax.chat/v1`은 HTTP 연결에는 성공하지만 다음 종료 안내만 +반환한다. + +```text +Guest 용 API Endpoint 서비스는 종료되었습니다. +``` + +따라서 문서에 적힌 공개 key와 guest endpoint로는 현재 모델 추론을 시험할 수 없다. +테스트하려면 팀에서 사용할 수 있는 A.X API endpoint와 key를 받거나, 아래 5절처럼 +공식 Light 모델을 로컬에서 실행해야 한다. 이 저장소는 종료된 guest 주소를 작동하는 +기본값으로 사용하지 않는다. + +## 3. 팀 A.X API 설정 + +API key는 코드, `.env.example`, 명령행 인자 또는 결과 파일에 기록하지 않는다. +사용하는 A.X 제공자에서 OpenAI 호환 endpoint와 key를 확인해 환경변수로만 설정한다. + +```bash +export AX_API_KEY="팀에서 발급받은 키" +export AX_BASE_URL="팀에서 전달받은 OpenAI 호환 base URL" +export AX_MODEL="팀에서 전달받은 모델명" +``` + +`ADOTX_API_KEY`도 호환한다. `.env` 파일은 Git에서 제외되지만 셸이 자동으로 읽지는 +않으므로 직접 `source .env` 하거나 환경변수를 내보내야 한다. + +## 4. API로 한 문장 테스트 + +실제 개인정보 대신 `WRK-001` 같은 더미 식별자를 사용한다. + +```bash +python -m fowoco_knowledge test-intent-ax \ + "WRK-001 체류기간 만료가 다가오니 여권 사본 받아줘" \ + --confirm-external +``` + +출력에서 다음 항목을 확인한다. + +- `requested_model`, `returned_model`: 요청·응답 모델명 +- `raw_content`: A.X 원문 응답 +- `parsed_output`: 추출된 JSON +- `parse_strategy`: 직접 JSON, 코드 펜스, 부가설명 속 JSON 여부 +- `issues`: Schema, evidence substring, 순서, 중복, OUT_OF_SCOPE 오류 + +구조 오류가 있으면 명령은 종료 코드 1을 반환한다. 모델 응답을 정답처럼 자동 +수정하지 않는다. + +## 5. API로 소량 Smoke Evaluation + +Validation 앞부분 5건: + +```bash +python -m fowoco_knowledge run-intent-ax-evaluation \ + --limit 5 \ + --delay-seconds 1 \ + --confirm-external +``` + +특정 원본 ID: + +```bash +python -m fowoco_knowledge run-intent-ax-evaluation \ + --ids 20,74,198,653,1206 \ + --delay-seconds 1 \ + --confirm-external +``` + +전체 Validation 268건은 제공자의 호출 제한과 비용을 확인한 뒤에만 실행한다. + +```bash +python -m fowoco_knowledge run-intent-ax-evaluation \ + --all-validation \ + --delay-seconds 1 \ + --confirm-external +``` + +결과는 기본적으로 Git에서 제외되는 다음 로컬 경로에 저장된다. + +```text +local-data/experiments/intent/ax-zero-shot-v1/ +├── predictions.jsonl +└── report.yaml +``` + +Report는 Intent Exact Match, Macro/Micro F1, evidence 정답 완전일치, 구조 Gate, +오류 건수와 token usage를 분리해 기록한다. + +## 6. 공식 A.X 4.0 Light를 로컬에서 직접 테스트 + +최초 1회 로컬 실행 의존성을 설치한다. + +```bash +pip install -e ".[ax-local]" +``` + +Apple Silicon Mac에서는 다음처럼 한 문장을 테스트한다. + +```bash +python -m fowoco_knowledge test-intent-ax-local \ + "WRK-001 체류기간 만료가 다가오니 여권 사본 받아줘" \ + --device mps \ + --confirm-model-download +``` + +`--confirm-model-download`는 공식 `skt/A.X-4.0-Light` 모델을 Hugging Face에서 +처음 내려받고 메모리에 올리는 작업을 명시적으로 허용한다. 7B BF16 원본 가중치는 +대용량이며, 24GB Mac에서도 다른 앱의 메모리 사용량에 따라 로딩에 실패할 수 있다. +CI에서는 이 명령을 실행하지 않는다. + +로컬 명령도 API 명령과 동일한 `raw_content`, `parsed_output`, `issues` 구조를 +출력하므로 Intent와 evidence 결과를 그대로 비교할 수 있다. + +## 7. GPU 서버의 A.X 4.0 Light에 연결 + +공식 모델 카드의 vLLM 예시처럼 GPU 서버에서 Light 모델을 OpenAI 호환 endpoint로 +실행한 경우 다음처럼 연결한다. + +```bash +vllm serve skt/A.X-4.0-Light + +export AX_BASE_URL="http://localhost:8000/v1" +export AX_MODEL="skt/A.X-4.0-Light" +export AX_API_KEY="local" + +python -m fowoco_knowledge test-intent-ax \ + "WRK-001 체류기간 연장 준비해줘" \ + --confirm-external +``` + +현재 개발 Mac은 Apple Silicon이므로 CUDA 기반 vLLM 서버를 직접 실행하는 환경과는 +다르다. Mac에서 quantized 모델을 별도로 구동할 경우에도 OpenAI 호환 endpoint만 +제공하면 같은 테스트 명령을 사용할 수 있다. + +## 8. 개인정보와 해석 제한 + +- 실제 외국인등록번호, 여권번호, 전화번호, 계좌번호를 전송하지 않는다. +- 코드의 기본 개인정보 패턴 검사는 보조 장치이며 사람 이름과 모든 식별자를 완전히 + 탐지하지 못한다. +- 종료된 guest API 연결 성공을 모델 추론 성공으로 표현하지 않는다. +- API의 `ax4` 결과를 `A.X-4.0-Light` 결과로 바꿔 표현하지 않는다. +- 수정 전 라벨로 얻은 점수는 consensus 반영 후 같은 prompt로 다시 측정한다. diff --git a/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md b/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md index e025f1e..c0871c6 100644 --- a/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md +++ b/fowoco-knowledge/docs/INTENT_MODELING_HANDOFF.md @@ -14,6 +14,7 @@ knowledge 저장소는 다음 자료를 제공한다. | 평가 정책 | `knowledge/intent_evaluation_policy.yaml` | baseline 전 임시 목표 | | Gold 계획 | `docs/INTENT_GOLD_TEST_PLAN.md` | 작성 전 | | 임시 baseline 가이드 | `docs/INTENT_PROVISIONAL_BASELINE.md` | 수정 전 라벨 실험 | +| A.X 테스트 가이드 | `docs/INTENT_AX_TESTING.md` | zero-shot smoke 준비 | AI 저장소는 프롬프트, 모델 어댑터, 추론·평가 코드와 실험 결과를 관리한다. diff --git a/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md b/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md index 034a5b9..12e38d3 100644 --- a/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md +++ b/fowoco-knowledge/docs/INTENT_PROVISIONAL_BASELINE.md @@ -26,6 +26,9 @@ Intent를 one-vs-rest 방식으로 학습하고 Train 내부 점수만으로 임 이 모델은 데이터·평가 파이프라인 검증용이며 A.X, BERT 또는 Transformer 후보를 대체하지 않는다. +실제 A.X zero-shot 테스트는 +[`INTENT_AX_TESTING.md`](INTENT_AX_TESTING.md)를 따른다. + ## 저장소 경계 AI 저장소의 작업 충돌을 피하기 위해 현재는 knowledge 저장소의 `model` 브랜치에서 diff --git a/fowoco-knowledge/experiments/intent/prompts/ax-zero-shot-v1.md b/fowoco-knowledge/experiments/intent/prompts/ax-zero-shot-v1.md new file mode 100644 index 0000000..9ce8d96 --- /dev/null +++ b/fowoco-knowledge/experiments/intent/prompts/ax-zero-shot-v1.md @@ -0,0 +1,65 @@ +당신은 FOWOCO의 HR 업무 요청 Intent 분류기입니다. +입력으로 주어진 hr_input만 분석하여 JSON 객체 하나만 출력하십시오. + +## Intent 정의 + +1. WORK_INSTRUCTION + - 작업 지시, 근무 일정 변경, 현장 행동 또는 연락 절차 안내 +2. DOCUMENT_REQUEST + - 특정 서류를 요청·수령하거나 미제출 상태를 추적하는 행위 +3. PAYROLL_EXPLANATION + - 급여, 수당, 공제, 명세 차이 또는 근태 반영 내역 설명 +4. WORKER_ONBOARDING + - 신규 근로자 등록 초안, 최초 보험 가입, 초기 프로필 처리 +5. EMPLOYMENT_CHANGE + - 퇴사, 무단결근·연락두절, 사업장 변경 등 고용상태 변동 확인과 신고 준비 +6. EXPIRY_RENEWAL + - 체류기간, 근로계약, 고용허가기간의 만료 확인과 연장·갱신 준비 +7. OUT_OF_SCOPE + - 위 6개 Intent에 해당하지 않거나 새로운 HR 요청이 없는 발화 + +## 분류 규칙 + +- 최종 목적을 추정하지 말고 발화에 명시된 현재 요청·확인·준비 행위를 분류합니다. +- DOCUMENT_REQUEST는 받아줘, 제출받아, 요청해, 미제출 확인, 첨부해줘처럼 + 서류 확보·추적 행위가 명시된 경우만 추가합니다. +- 이미 첨부·업로드된 서류를 사용하는 경우 DOCUMENT_REQUEST를 추가하지 않습니다. +- 서류 확보와 목적 업무가 모두 명시되면 두 Intent를 모두 출력합니다. +- 외부기관 접수·신고·제출은 별도 Intent가 아닙니다. 그 표현만으로 + DOCUMENT_REQUEST를 추가하거나 evidence에 포함하지 않습니다. +- 일반 휴가라는 단어만으로 EMPLOYMENT_CHANGE를 추가하지 않습니다. +- 통장사본 요청은 DOCUMENT_REQUEST이며, 급여계좌 등록 자체는 + PAYROLL_EXPLANATION이 아닙니다. +- 새로운 요청이나 확인이 없는 순수 완료·상태 보고는 OUT_OF_SCOPE입니다. +- OUT_OF_SCOPE는 다른 Intent와 함께 출력하지 않고 evidence는 null입니다. + +## evidence 규칙 + +- hr_input에 토씨까지 동일하게 존재하는 연속 부분 문자열이어야 합니다. +- 해당 Intent를 판단할 수 있는 가장 짧고 완결된 표현을 선택합니다. +- 다른 Intent의 근거나 외부기관 실행 표현을 포함하지 않습니다. +- Multi-Intent 배열은 evidence가 원문에 처음 나타난 순서로 정렬합니다. + +## 출력 형식 + +설명, 마크다운, 코드 펜스 없이 다음 구조의 JSON 객체 하나만 출력합니다. + +{ + "intents": [ + { + "intent": "INTENT_CODE", + "evidence": "hr_input의 정확한 연속 부분 문자열" + } + ] +} + +OUT_OF_SCOPE인 경우: + +{ + "intents": [ + { + "intent": "OUT_OF_SCOPE", + "evidence": null + } + ] +} diff --git a/fowoco-knowledge/pyproject.toml b/fowoco-knowledge/pyproject.toml index f3dfc46..b35d990 100644 --- a/fowoco-knowledge/pyproject.toml +++ b/fowoco-knowledge/pyproject.toml @@ -15,6 +15,12 @@ dependencies = [ ] [project.optional-dependencies] +ax-local = [ + "accelerate>=1,<2", + "safetensors>=0.4,<1", + "torch>=2.3,<3", + "transformers>=4.46,<5", +] dev = [ "pytest>=8.3,<10", "pytest-cov>=6,<8", diff --git a/fowoco-knowledge/src/fowoco_knowledge/ax_intent.py b/fowoco-knowledge/src/fowoco_knowledge/ax_intent.py new file mode 100644 index 0000000..1669212 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/ax_intent.py @@ -0,0 +1,636 @@ +from __future__ import annotations + +import json +import os +import re +import time +import urllib.error +import urllib.request +from collections import Counter +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +from .ingestion import file_sha256 +from .intent_model import validate_intent_model_output +from .intent_split import load_intent_cases, read_split_ids +from .provisional_baseline import ( + compact_json, + evaluate_prediction_outputs, +) + +DEFAULT_AX_MODEL = "ax4" +DEFAULT_AX_LOCAL_MODEL = "skt/A.X-4.0-Light" +DEFAULT_AX_PROMPT_PATH = Path("experiments/intent/prompts/ax-zero-shot-v1.md") +DEFAULT_AX_OUTPUT_DIR = Path("local-data/experiments/intent/ax-zero-shot-v1") +SENSITIVE_INPUT_PATTERNS = { + "resident_or_alien_registration_number": re.compile(r"(? str: + base_url = self.base_url.rstrip("/") + if base_url.endswith("/chat/completions"): + return base_url + return f"{base_url}/chat/completions" + + +@dataclass(frozen=True) +class AXClassification: + requested_model: str + returned_model: str | None + response_id: str | None + raw_content: str + parsed_output: Any + parse_strategy: str | None + issues: tuple[dict[str, str], ...] + usage: dict[str, Any] + + def to_dict(self) -> dict[str, Any]: + return { + "requested_model": self.requested_model, + "returned_model": self.returned_model, + "response_id": self.response_id, + "raw_content": self.raw_content, + "parsed_output": self.parsed_output, + "parse_strategy": self.parse_strategy, + "issues": list(self.issues), + "usage": self.usage, + } + + +def load_ax_config( + *, + base_url: str | None = None, + model: str | None = None, + timeout_seconds: float = 90.0, + temperature: float = 0.0, + max_tokens: int = 512, +) -> AXConfig: + api_key = os.getenv("AX_API_KEY") or os.getenv("ADOTX_API_KEY") + if not api_key: + raise AXIntentError("AX_API_KEY 또는 ADOTX_API_KEY 환경변수를 설정해야 합니다.") + resolved_base_url = base_url or os.getenv("AX_BASE_URL") + if not resolved_base_url: + raise AXIntentError( + "AX_BASE_URL 또는 --base-url을 설정해야 합니다. " + "공식 문서의 guest endpoint는 현재 종료 상태입니다." + ) + return AXConfig( + base_url=resolved_base_url, + model=model or os.getenv("AX_MODEL") or DEFAULT_AX_MODEL, + api_key=api_key, + timeout_seconds=timeout_seconds, + temperature=temperature, + max_tokens=max_tokens, + ) + + +def reject_sensitive_input(hr_input: str) -> None: + matches = [ + name for name, pattern in SENSITIVE_INPUT_PATTERNS.items() if pattern.search(hr_input) + ] + if matches: + raise AXIntentError("A.X 테스트에 사용할 수 없는 개인정보 패턴: " + ", ".join(matches)) + + +def parse_ax_json_content(content: str) -> tuple[Any, str]: + stripped = content.strip() + candidates: list[tuple[str, str]] = [(stripped, "direct_json")] + fenced = re.fullmatch( + r"```(?:json)?\s*(.*?)\s*```", + stripped, + flags=re.DOTALL | re.IGNORECASE, + ) + if fenced: + candidates.insert(0, (fenced.group(1).strip(), "markdown_fence")) + first_brace = stripped.find("{") + last_brace = stripped.rfind("}") + if first_brace >= 0 and last_brace > first_brace: + candidates.append( + ( + stripped[first_brace : last_brace + 1], + "embedded_json_object", + ) + ) + + attempted: set[str] = set() + for candidate, strategy in candidates: + if candidate in attempted: + continue + attempted.add(candidate) + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if not isinstance(parsed, dict): + raise AXIntentError("A.X JSON 응답의 최상위 값이 객체가 아닙니다.") + return parsed, strategy + raise AXIntentError("A.X 응답에서 유효한 JSON 객체를 찾지 못했습니다.") + + +def _default_transport( + request: urllib.request.Request, + timeout_seconds: float, +) -> dict[str, Any]: + try: + with urllib.request.urlopen( + request, + timeout=timeout_seconds, + ) as response: + raw = response.read().decode("utf-8") + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + raise AXIntentError(f"A.X API HTTP {error.code}: {body[:500]}") from error + except urllib.error.URLError as error: + raise AXIntentError(f"A.X API 연결 실패: {error.reason}") from error + try: + payload = json.loads(raw) + except json.JSONDecodeError as error: + raise AXIntentError("A.X API가 JSON이 아닌 응답을 반환했습니다.") from error + if not isinstance(payload, dict): + raise AXIntentError("A.X API 응답의 최상위 값이 객체가 아닙니다.") + return payload + + +class AXIntentClient: + def __init__( + self, + config: AXConfig, + *, + transport: Transport | None = None, + ) -> None: + self.config = config + self.transport = transport or _default_transport + + def classify( + self, + *, + hr_input: str, + system_prompt: str, + output_schema: dict[str, Any], + ) -> AXClassification: + reject_sensitive_input(hr_input) + user_content = "다음 JSON의 hr_input을 분류하십시오.\n" + compact_json( + {"hr_input": hr_input} + ) + request_payload = { + "model": self.config.model, + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content}, + ], + "temperature": self.config.temperature, + "max_tokens": self.config.max_tokens, + } + request = urllib.request.Request( + self.config.chat_completions_url, + data=json.dumps(request_payload).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.config.api_key}", + }, + method="POST", + ) + response = self.transport(request, self.config.timeout_seconds) + if "choices" not in response and isinstance( + response.get("message"), + dict | str, + ): + service_message = response["message"] + if isinstance(service_message, dict): + rendered_message = " / ".join( + str(service_message[key]) for key in ("ko", "en") if service_message.get(key) + ) + else: + rendered_message = service_message + raise AXIntentError(f"A.X API 서비스 메시지: {rendered_message}") + try: + message = response["choices"][0]["message"] + raw_content = message["content"] + except (KeyError, IndexError, TypeError) as error: + raise AXIntentError( + "A.X API 응답에서 choices[0].message.content를 찾지 못했습니다." + ) from error + if not isinstance(raw_content, str): + raise AXIntentError("A.X message.content가 문자열이 아닙니다.") + + parsed_output: Any = None + parse_strategy: str | None = None + issues: list[dict[str, str]] = [] + try: + parsed_output, parse_strategy = parse_ax_json_content(raw_content) + except AXIntentError as error: + issues.append({"code": "INVALID_JSON", "message": str(error)}) + if parsed_output is not None: + issues.extend( + { + "code": issue.code, + "message": issue.message, + } + for issue in validate_intent_model_output( + hr_input, + parsed_output, + output_schema, + ) + ) + usage = response.get("usage") + return AXClassification( + requested_model=self.config.model, + returned_model=( + response.get("model") if isinstance(response.get("model"), str) else None + ), + response_id=(response.get("id") if isinstance(response.get("id"), str) else None), + raw_content=raw_content, + parsed_output=parsed_output, + parse_strategy=parse_strategy, + issues=tuple(issues), + usage=usage if isinstance(usage, dict) else {}, + ) + + +def _resolve_local_device(torch_module: Any, requested_device: str) -> str: + if requested_device != "auto": + if requested_device not in {"cuda", "mps", "cpu"}: + raise AXIntentError("--device는 auto, cuda, mps, cpu 중 하나여야 합니다.") + return requested_device + if torch_module.cuda.is_available(): + return "cuda" + mps = getattr(torch_module.backends, "mps", None) + if mps is not None and mps.is_available(): + return "mps" + return "cpu" + + +def classify_with_local_transformers( + *, + hr_input: str, + system_prompt: str, + output_schema: dict[str, Any], + model_id: str = DEFAULT_AX_LOCAL_MODEL, + device: str = "auto", + max_new_tokens: int = 512, +) -> AXClassification: + """Run the official A.X model locally after an explicit CLI confirmation.""" + reject_sensitive_input(hr_input) + if max_new_tokens < 1: + raise AXIntentError("--max-new-tokens는 1 이상이어야 합니다.") + try: + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer + except ImportError as error: + raise AXIntentError( + '로컬 A.X 실행 의존성이 없습니다. pip install -e ".[ax-local]"을 먼저 실행하세요.' + ) from error + + resolved_device = _resolve_local_device(torch, device) + if resolved_device == "cuda" and not torch.cuda.is_available(): + raise AXIntentError("CUDA를 사용할 수 없습니다.") + mps = getattr(torch.backends, "mps", None) + if resolved_device == "mps" and (mps is None or not mps.is_available()): + raise AXIntentError("MPS를 사용할 수 없습니다.") + + if resolved_device == "cuda": + dtype = torch.bfloat16 + elif resolved_device == "mps": + dtype = torch.float16 + else: + dtype = torch.float32 + + try: + tokenizer = AutoTokenizer.from_pretrained(model_id) + model = AutoModelForCausalLM.from_pretrained( + model_id, + torch_dtype=dtype, + low_cpu_mem_usage=True, + device_map={"": resolved_device}, + ) + model.eval() + except Exception as error: + raise AXIntentError(f"로컬 A.X 모델을 불러오지 못했습니다: {error}") from error + + user_content = "다음 JSON의 hr_input을 분류하십시오.\n" + compact_json({"hr_input": hr_input}) + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content}, + ] + try: + input_ids = tokenizer.apply_chat_template( + messages, + add_generation_prompt=True, + return_tensors="pt", + ).to(resolved_device) + with torch.inference_mode(): + generated_ids = model.generate( + input_ids, + max_new_tokens=max_new_tokens, + do_sample=False, + pad_token_id=tokenizer.eos_token_id, + ) + completion_ids = generated_ids[0, input_ids.shape[-1] :] + raw_content = tokenizer.decode( + completion_ids, + skip_special_tokens=True, + ).strip() + except Exception as error: + raise AXIntentError(f"로컬 A.X 추론에 실패했습니다: {error}") from error + + parsed_output: Any = None + parse_strategy: str | None = None + issues: list[dict[str, str]] = [] + try: + parsed_output, parse_strategy = parse_ax_json_content(raw_content) + except AXIntentError as error: + issues.append({"code": "INVALID_JSON", "message": str(error)}) + if parsed_output is not None: + issues.extend( + { + "code": issue.code, + "message": issue.message, + } + for issue in validate_intent_model_output( + hr_input, + parsed_output, + output_schema, + ) + ) + + return AXClassification( + requested_model=model_id, + returned_model=model_id, + response_id="local-transformers", + raw_content=raw_content, + parsed_output=parsed_output, + parse_strategy=parse_strategy, + issues=tuple(issues), + usage={ + "prompt_tokens": int(input_ids.shape[-1]), + "completion_tokens": int(completion_ids.shape[-1]), + "total_tokens": int(input_ids.shape[-1] + completion_ids.shape[-1]), + }, + ) + + +def parse_case_ids(raw: str | None) -> tuple[int, ...] | None: + if raw is None: + return None + try: + ids = tuple(int(item.strip()) for item in raw.split(",") if item.strip()) + except ValueError as error: + raise AXIntentError("--ids는 쉼표로 구분한 정수여야 합니다.") from error + if not ids: + raise AXIntentError("--ids에 하나 이상의 ID가 필요합니다.") + if len(ids) != len(set(ids)): + raise AXIntentError("--ids에 중복 ID가 있습니다.") + return ids + + +def load_ax_prompt( + project_root: Path, + prompt_path: Path | None = None, +) -> tuple[Path, str]: + resolved = ( + prompt_path.resolve() + if prompt_path is not None and prompt_path.is_absolute() + else project_root.resolve() / (prompt_path or DEFAULT_AX_PROMPT_PATH) + ) + try: + resolved.relative_to(project_root.resolve()) + except ValueError as error: + raise AXIntentError("A.X prompt 파일은 knowledge 프로젝트 안에 있어야 합니다.") from error + if not resolved.is_file(): + raise AXIntentError(f"A.X prompt 파일을 찾을 수 없습니다: {resolved}") + return resolved, resolved.read_text(encoding="utf-8") + + +def _select_cases( + *, + source_cases: list[dict[str, Any]], + validation_ids: tuple[int, ...], + case_ids: tuple[int, ...] | None, + limit: int | None, + all_validation: bool, +) -> tuple[list[dict[str, Any]], str]: + cases_by_id = {case["id"]: case for case in source_cases} + if case_ids is not None: + missing = [case_id for case_id in case_ids if case_id not in cases_by_id] + if missing: + raise AXIntentError(f"원본에 없는 ID: {missing[:5]}") + return [cases_by_id[case_id] for case_id in case_ids], "selected_ids" + if all_validation: + if limit is not None: + raise AXIntentError("--all-validation과 --limit은 함께 사용할 수 없습니다.") + return [cases_by_id[case_id] for case_id in validation_ids], "validation_all" + if limit is None or limit < 1: + raise AXIntentError( + "호출량 보호를 위해 --limit N, --ids 또는 --all-validation이 필요합니다." + ) + return ( + [cases_by_id[case_id] for case_id in validation_ids[:limit]], + "validation_limited", + ) + + +def _resolve_ax_output_dir( + *, + project_root: Path, + output_dir: Path | None, +) -> Path: + if output_dir is None: + return project_root.parent / DEFAULT_AX_OUTPUT_DIR + return ( + output_dir.resolve() + if output_dir.is_absolute() + else (project_root.parent / output_dir).resolve() + ) + + +def run_ax_evaluation( + *, + project_root: Path, + client: AXIntentClient, + prompt_path: Path | None = None, + case_ids: tuple[int, ...] | None = None, + limit: int | None = None, + all_validation: bool = False, + delay_seconds: float = 0.5, + output_dir: Path | None = None, +) -> dict[str, Any]: + if delay_seconds < 0: + raise AXIntentError("--delay-seconds는 0 이상이어야 합니다.") + project_root = project_root.resolve() + resolved_prompt_path, system_prompt = load_ax_prompt( + project_root, + prompt_path, + ) + output_schema_path = project_root / "schemas/intent-model-output.schema.json" + output_schema = json.loads(output_schema_path.read_text(encoding="utf-8")) + + intent_manifest = yaml.safe_load( + (project_root / "data/intent/manifest.yaml").read_text(encoding="utf-8") + ) + source_path = project_root / intent_manifest["path"] + if file_sha256(source_path) != intent_manifest["sha256"]: + raise AXIntentError("Intent 원본 checksum이 manifest와 다릅니다.") + split_manifest_path = project_root / "data/intent/splits/provisional-v1/manifest.yaml" + split_manifest = yaml.safe_load(split_manifest_path.read_text(encoding="utf-8")) + validation_path = project_root / split_manifest["outputs"]["validation"]["path"] + if file_sha256(validation_path) != split_manifest["outputs"]["validation"]["sha256"]: + raise AXIntentError("Validation ID checksum이 manifest와 다릅니다.") + + source_cases = load_intent_cases(source_path) + validation_ids = read_split_ids(validation_path) + selected_cases, evaluation_scope = _select_cases( + source_cases=source_cases, + validation_ids=validation_ids, + case_ids=case_ids, + limit=limit, + all_validation=all_validation, + ) + + outputs: list[Any] = [] + api_rows: list[dict[str, Any]] = [] + failure_count = 0 + returned_models: Counter[str] = Counter() + total_usage: Counter[str] = Counter() + for index, case in enumerate(selected_cases): + if index and delay_seconds: + time.sleep(delay_seconds) + try: + classification = client.classify( + hr_input=case["hr_input"], + system_prompt=system_prompt, + output_schema=output_schema, + ) + except AXIntentError as error: + failure_count += 1 + outputs.append(None) + api_rows.append( + { + "id": case["id"], + "hr_input": case["hr_input"], + "expected": {"intents": case["intents"]}, + "requested_model": client.config.model, + "returned_model": None, + "response_id": None, + "raw_content": "", + "parsed_output": None, + "parse_strategy": None, + "issues": [{"code": "API_ERROR", "message": str(error)}], + "usage": {}, + } + ) + continue + outputs.append(classification.parsed_output) + if classification.parsed_output is None: + failure_count += 1 + if classification.returned_model: + returned_models[classification.returned_model] += 1 + for key, value in classification.usage.items(): + if isinstance(value, int): + total_usage[key] += value + api_rows.append( + { + "id": case["id"], + "hr_input": case["hr_input"], + "expected": {"intents": case["intents"]}, + **classification.to_dict(), + } + ) + + metrics, evaluated_predictions = evaluate_prediction_outputs( + cases=selected_cases, + outputs=outputs, + output_schema=output_schema, + ) + evaluation_by_id = {row["id"]: row for row in evaluated_predictions} + for row in api_rows: + evaluated = evaluation_by_id[row["id"]] + row["structural_issues"] = evaluated["structural_issues"] + + resolved_output_dir = _resolve_ax_output_dir( + project_root=project_root, + output_dir=output_dir, + ) + resolved_output_dir.mkdir(parents=True, exist_ok=True) + predictions_path = resolved_output_dir / "predictions.jsonl" + report_path = resolved_output_dir / "report.yaml" + with predictions_path.open("w", encoding="utf-8") as handle: + for row in api_rows: + handle.write(compact_json(row) + "\n") + + report = { + "experiment_id": "FOWOCO-INTENT-AX-ZERO-SHOT-V1", + "version": "0.1.0", + "status": "provisional_pre_consensus", + "provider": { + "base_url": client.config.base_url, + "requested_model": client.config.model, + "returned_model_counts": dict(sorted(returned_models.items())), + "api_key_stored": False, + }, + "prompt": { + "path": str(resolved_prompt_path.relative_to(project_root)), + "sha256": file_sha256(resolved_prompt_path), + }, + "source": { + "path": intent_manifest["path"], + "status": intent_manifest["status"], + "sha256": intent_manifest["sha256"], + }, + "evaluation": { + "scope": evaluation_scope, + "record_count": len(selected_cases), + "case_ids": [case["id"] for case in selected_cases], + "failure_count": failure_count, + "delay_seconds": delay_seconds, + "metrics": metrics, + }, + "usage": dict(sorted(total_usage.items())), + "artifacts": { + "predictions": { + "path": str(predictions_path), + "record_count": len(api_rows), + "sha256": file_sha256(predictions_path), + } + }, + "claims_not_allowed": [ + "최종 모델 성능", + "Gold Test 성능", + "A.X-4.0-Light 성능(반환 모델이 Light로 확인되지 않은 경우)", + "A/B consensus가 반영된 라벨 성능", + ], + "limitations": [ + "수정 전 recheck_required 라벨을 기준으로 평가함", + "외부 API 응답은 동일 요청에도 달라질 수 있음", + "제한 실행 결과는 전체 Validation 성능이 아님", + "실제 개인정보나 기업정보를 외부 API로 전송하지 않음", + ], + } + report_path.write_text( + yaml.safe_dump(report, allow_unicode=True, sort_keys=False), + encoding="utf-8", + ) + return report diff --git a/fowoco-knowledge/src/fowoco_knowledge/cli.py b/fowoco-knowledge/src/fowoco_knowledge/cli.py index 8d8c8fb..4ca0380 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/cli.py +++ b/fowoco-knowledge/src/fowoco_knowledge/cli.py @@ -5,6 +5,16 @@ from collections.abc import Sequence from pathlib import Path +from .ax_intent import ( + DEFAULT_AX_LOCAL_MODEL, + AXIntentClient, + AXIntentError, + classify_with_local_transformers, + load_ax_config, + load_ax_prompt, + parse_case_ids, + run_ax_evaluation, +) from .engine import RequestEvaluator from .ingestion import OfficialDataPipeline from .intent_split import ( @@ -17,6 +27,30 @@ from .validation import KnowledgeValidator +def add_ax_connection_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--base-url", + help="OpenAI-compatible base URL (or AX_BASE_URL)", + ) + parser.add_argument( + "--model", + help="Provider model name (or AX_MODEL)", + ) + parser.add_argument("--timeout", type=float, default=90.0) + parser.add_argument("--temperature", type=float, default=0.0) + parser.add_argument("--max-tokens", type=int, default=512) + parser.add_argument( + "--prompt", + type=Path, + help="Prompt file inside the knowledge project", + ) + parser.add_argument( + "--confirm-external", + action="store_true", + help="Confirm that dummy input may be sent to the configured external API", + ) + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="FOWOCO knowledge tools") parser.add_argument("--root", help="fowoco-knowledge project root") @@ -94,6 +128,66 @@ def build_parser() -> argparse.ArgumentParser: type=Path, help="Output directory inside the knowledge project", ) + + ax_test_parser = subparsers.add_parser( + "test-intent-ax", + help="Classify one dummy HR utterance with an A.X-compatible endpoint", + ) + ax_test_parser.add_argument("hr_input") + add_ax_connection_arguments(ax_test_parser) + + ax_local_parser = subparsers.add_parser( + "test-intent-ax-local", + help="Classify one dummy HR utterance with local official A.X-4.0-Light", + ) + ax_local_parser.add_argument("hr_input") + ax_local_parser.add_argument( + "--model-id", + default=DEFAULT_AX_LOCAL_MODEL, + help=f"Hugging Face model ID (default: {DEFAULT_AX_LOCAL_MODEL})", + ) + ax_local_parser.add_argument( + "--device", + default="auto", + choices=("auto", "cuda", "mps", "cpu"), + ) + ax_local_parser.add_argument("--max-new-tokens", type=int, default=512) + ax_local_parser.add_argument( + "--prompt", + type=Path, + help="Prompt file inside the knowledge project", + ) + ax_local_parser.add_argument( + "--confirm-model-download", + action="store_true", + help="Confirm that the large model may be downloaded and loaded locally", + ) + + ax_evaluation_parser = subparsers.add_parser( + "run-intent-ax-evaluation", + help="Run a limited or explicit provisional A.X evaluation", + ) + ax_evaluation_parser.add_argument( + "--ids", + help="Comma-separated source record IDs", + ) + ax_evaluation_parser.add_argument("--limit", type=int) + ax_evaluation_parser.add_argument( + "--all-validation", + action="store_true", + help="Explicitly run all 268 provisional Validation records", + ) + ax_evaluation_parser.add_argument( + "--delay-seconds", + type=float, + default=0.5, + ) + ax_evaluation_parser.add_argument( + "--output-dir", + type=Path, + help="Local output directory (default: ignored local-data path)", + ) + add_ax_connection_arguments(ax_evaluation_parser) return parser @@ -180,4 +274,79 @@ def main(argv: Sequence[str] | None = None) -> int: print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 + if args.command == "test-intent-ax-local": + if not args.confirm_model_download: + print( + "BLOCKED: 대용량 A.X 모델 다운로드·로딩을 확인하려면 " + "--confirm-model-download를 지정하세요." + ) + return 2 + try: + _, system_prompt = load_ax_prompt(repository.root, args.prompt) + output_schema = repository.load_json("schemas/intent-model-output.schema.json") + classification = classify_with_local_transformers( + hr_input=args.hr_input, + system_prompt=system_prompt, + output_schema=output_schema, + model_id=args.model_id, + device=args.device, + max_new_tokens=args.max_new_tokens, + ) + result = { + "hr_input": args.hr_input, + **classification.to_dict(), + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 1 if classification.issues else 0 + except AXIntentError as error: + print(f"ERROR: {error}") + return 1 + + if args.command in {"test-intent-ax", "run-intent-ax-evaluation"}: + if not args.confirm_external: + print("BLOCKED: 외부 API 전송을 확인하려면 --confirm-external을 지정하세요.") + return 2 + try: + config = load_ax_config( + base_url=args.base_url, + model=args.model, + timeout_seconds=args.timeout, + temperature=args.temperature, + max_tokens=args.max_tokens, + ) + client = AXIntentClient(config) + if args.command == "test-intent-ax": + _, system_prompt = load_ax_prompt( + repository.root, + args.prompt, + ) + output_schema = repository.load_json("schemas/intent-model-output.schema.json") + classification = client.classify( + hr_input=args.hr_input, + system_prompt=system_prompt, + output_schema=output_schema, + ) + result = { + "hr_input": args.hr_input, + **classification.to_dict(), + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 1 if classification.issues else 0 + + report = run_ax_evaluation( + project_root=repository.root, + client=client, + prompt_path=args.prompt, + case_ids=parse_case_ids(args.ids), + limit=args.limit, + all_validation=args.all_validation, + delay_seconds=args.delay_seconds, + output_dir=args.output_dir, + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + return 0 + except AXIntentError as error: + print(f"ERROR: {error}") + return 1 + return 2 diff --git a/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py b/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py index cd398ed..65bfba8 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py +++ b/fowoco-knowledge/src/fowoco_knowledge/provisional_baseline.py @@ -348,6 +348,22 @@ def evaluate_baseline( validation_cases: list[dict[str, Any]], output_schema: dict[str, Any], ) -> tuple[dict[str, Any], list[dict[str, Any]]]: + outputs = [model.predict(case["hr_input"]) for case in validation_cases] + return evaluate_prediction_outputs( + cases=validation_cases, + outputs=outputs, + output_schema=output_schema, + ) + + +def evaluate_prediction_outputs( + *, + cases: list[dict[str, Any]], + outputs: list[Any], + output_schema: dict[str, Any], +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + if len(cases) != len(outputs): + raise ProvisionalBaselineError("case and prediction counts differ") label_counts = {intent: Counter() for intent in INTENT_CODES} structural_failures: Counter[str] = Counter() error_counts: Counter[str] = Counter() @@ -356,8 +372,7 @@ def evaluate_baseline( evidence_exact_matches = 0 evidence_expected_count = 0 - for case in validation_cases: - output = model.predict(case["hr_input"]) + for case, output in zip(cases, outputs, strict=True): issues = validate_intent_model_output( case["hr_input"], output, @@ -367,7 +382,16 @@ def evaluate_baseline( structural_failures.update(issue_codes) expected_codes = [item["intent"] for item in case["intents"]] - predicted_codes = [item["intent"] for item in output["intents"]] + predicted_items = ( + output.get("intents", []) + if isinstance(output, dict) and isinstance(output.get("intents"), list) + else [] + ) + predicted_codes = [ + item["intent"] + for item in predicted_items + if isinstance(item, dict) and isinstance(item.get("intent"), str) + ] expected_set = set(expected_codes) predicted_set = set(predicted_codes) _metric_counts(expected_set, predicted_set, label_counts) @@ -385,8 +409,10 @@ def evaluate_baseline( } predicted_evidence = { item["intent"]: item["evidence"] - for item in output["intents"] - if item["intent"] != "OUT_OF_SCOPE" + for item in predicted_items + if isinstance(item, dict) + and item.get("intent") != "OUT_OF_SCOPE" + and isinstance(item.get("intent"), str) } for intent, evidence in expected_evidence.items(): evidence_expected_count += 1 @@ -410,7 +436,7 @@ def evaluate_baseline( ) per_intent, macro_f1, micro_f1 = _metrics_from_counts(label_counts) - record_count = len(validation_cases) + record_count = len(cases) metrics = { "record_count": record_count, "intent_exact_match": round( diff --git a/fowoco-knowledge/tests/test_ax_intent.py b/fowoco-knowledge/tests/test_ax_intent.py new file mode 100644 index 0000000..d12ef93 --- /dev/null +++ b/fowoco-knowledge/tests/test_ax_intent.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +import json +import urllib.request +from pathlib import Path + +import pytest +import yaml + +from fowoco_knowledge.ax_intent import ( + AXClassification, + AXConfig, + AXIntentClient, + AXIntentError, + classify_with_local_transformers, + load_ax_config, + load_ax_prompt, + parse_ax_json_content, + reject_sensitive_input, + run_ax_evaluation, +) +from fowoco_knowledge.cli import main +from fowoco_knowledge.repository import KnowledgeRepository + +ROOT = Path(__file__).resolve().parents[1] + + +def test_ax_json_parser_handles_direct_and_wrapped_objects() -> None: + expected = { + "intents": [ + { + "intent": "DOCUMENT_REQUEST", + "evidence": "여권 사본 받아줘", + } + ] + } + raw_json = json.dumps(expected, ensure_ascii=False) + + assert parse_ax_json_content(raw_json) == (expected, "direct_json") + assert parse_ax_json_content(f"```json\n{raw_json}\n```") == ( + expected, + "markdown_fence", + ) + assert parse_ax_json_content(f"분류 결과입니다.\n{raw_json}") == ( + expected, + "embedded_json_object", + ) + with pytest.raises(AXIntentError, match="유효한 JSON"): + parse_ax_json_content("JSON이 아닌 응답") + + +def test_ax_client_uses_openai_compatible_contract_without_exposing_key() -> None: + captured: dict[str, object] = {} + + def fake_transport( + request: urllib.request.Request, + timeout_seconds: float, + ) -> dict[str, object]: + captured["url"] = request.full_url + captured["authorization"] = request.get_header("Authorization") + captured["payload"] = json.loads(request.data or b"{}") + captured["timeout"] = timeout_seconds + return { + "id": "chatcmpl-test", + "model": "ax4", + "choices": [ + { + "message": { + "content": ( + '{"intents":[{"intent":"DOCUMENT_REQUEST",' + '"evidence":"여권 사본 받아줘"}]}' + ) + } + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 8, + "total_tokens": 18, + }, + } + + config = AXConfig( + base_url="https://example.test/v1", + model="ax4", + api_key="unit-test-secret", + timeout_seconds=12, + ) + client = AXIntentClient(config, transport=fake_transport) + schema = KnowledgeRepository(ROOT).load_json("schemas/intent-model-output.schema.json") + result = client.classify( + hr_input="여권 사본 받아줘", + system_prompt="JSON만 출력", + output_schema=schema, + ) + + assert isinstance(result, AXClassification) + assert result.issues == () + assert result.parsed_output["intents"][0]["intent"] == "DOCUMENT_REQUEST" + assert captured["url"] == "https://example.test/v1/chat/completions" + assert captured["authorization"] == "Bearer unit-test-secret" + assert captured["timeout"] == 12 + payload = captured["payload"] + assert isinstance(payload, dict) + assert payload["model"] == "ax4" + assert payload["temperature"] == 0.0 + assert "unit-test-secret" not in json.dumps(result.to_dict()) + assert "unit-test-secret" not in json.dumps(payload) + + +def test_ax_config_requires_environment_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AX_API_KEY", raising=False) + monkeypatch.delenv("ADOTX_API_KEY", raising=False) + with pytest.raises(AXIntentError, match="환경변수"): + load_ax_config() + + monkeypatch.setenv("AX_API_KEY", "test-only") + config = load_ax_config( + base_url="http://localhost:8000/v1", + model="skt/A.X-4.0-Light", + ) + assert config.model == "skt/A.X-4.0-Light" + assert config.chat_completions_url.endswith("/chat/completions") + assert "test-only" not in repr(config) + + +def test_ax_config_requires_explicit_base_url( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("AX_API_KEY", "test-only") + monkeypatch.delenv("AX_BASE_URL", raising=False) + with pytest.raises(AXIntentError, match="guest endpoint는 현재 종료"): + load_ax_config() + + +def test_ax_client_surfaces_provider_service_message() -> None: + def fake_transport( + request: urllib.request.Request, + timeout_seconds: float, + ) -> dict[str, object]: + del request, timeout_seconds + return { + "message": { + "ko": "Guest 용 API Endpoint 서비스는 종료되었습니다.", + "en": "The guest API endpoint service is no longer available.", + } + } + + client = AXIntentClient( + AXConfig( + base_url="https://example.test/v1", + model="ax4", + api_key="test-only", + ), + transport=fake_transport, + ) + schema = KnowledgeRepository(ROOT).load_json("schemas/intent-model-output.schema.json") + with pytest.raises(AXIntentError, match="서비스 메시지.*종료"): + client.classify( + hr_input="WRK-001 계약 갱신 준비", + system_prompt="JSON만 출력", + output_schema=schema, + ) + + +def test_ax_input_privacy_guard_rejects_identifier_patterns() -> None: + reject_sensitive_input("WRK-001 계약 만료 확인") + reject_sensitive_input("2026-07-27까지 제출") + with pytest.raises(AXIntentError, match="mobile_phone_number"): + reject_sensitive_input("연락처는 010-1234-5678입니다") + with pytest.raises(AXIntentError, match="passport_number"): + reject_sensitive_input("여권번호 M12345678 확인") + + +def test_ax_evaluation_writes_local_report_without_api_key( + tmp_path: Path, +) -> None: + source_cases = { + case["hr_input"]: case + for case in ( + json.loads(line) + for line in (ROOT / "data/intent/hr_intent_dataset.jsonl") + .read_text(encoding="utf-8") + .splitlines() + if line + ) + } + + def fake_transport( + request: urllib.request.Request, + timeout_seconds: float, + ) -> dict[str, object]: + del timeout_seconds + payload = json.loads(request.data or b"{}") + user_content = payload["messages"][1]["content"] + hr_input = json.loads(user_content.splitlines()[-1])["hr_input"] + case = source_cases[hr_input] + return { + "id": f"chatcmpl-{case['id']}", + "model": "ax4", + "choices": [ + { + "message": { + "content": json.dumps( + {"intents": case["intents"]}, + ensure_ascii=False, + ) + } + } + ], + "usage": {"total_tokens": 20}, + } + + client = AXIntentClient( + AXConfig( + base_url="https://example.test/v1", + model="ax4", + api_key="must-not-be-written", + ), + transport=fake_transport, + ) + output_dir = tmp_path / "ax-smoke" + report = run_ax_evaluation( + project_root=ROOT, + client=client, + limit=3, + delay_seconds=0, + output_dir=output_dir, + ) + + assert report["evaluation"]["record_count"] == 3 + assert report["evaluation"]["failure_count"] == 0 + assert report["evaluation"]["metrics"]["intent_exact_match"] == 1.0 + assert report["provider"]["api_key_stored"] is False + report_text = (output_dir / "report.yaml").read_text(encoding="utf-8") + predictions_text = (output_dir / "predictions.jsonl").read_text(encoding="utf-8") + assert "must-not-be-written" not in report_text + assert "must-not-be-written" not in predictions_text + assert yaml.safe_load(report_text)["status"] == "provisional_pre_consensus" + + +def test_ax_cli_requires_explicit_external_confirmation() -> None: + assert ( + main( + [ + "--root", + str(ROOT), + "test-intent-ax", + "WRK-001 여권 사본 받아줘", + ] + ) + == 2 + ) + + +def test_ax_local_cli_requires_explicit_model_download_confirmation() -> None: + assert ( + main( + [ + "--root", + str(ROOT), + "test-intent-ax-local", + "WRK-001 여권 사본 받아줘", + ] + ) + == 2 + ) + + +def test_ax_local_missing_dependencies_returns_actionable_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def block_import(name: str, *args: object, **kwargs: object) -> object: + del args, kwargs + if name in {"torch", "transformers"}: + raise ImportError(name) + return original_import(name) + + original_import = __import__ + monkeypatch.setattr("builtins.__import__", block_import) + schema = KnowledgeRepository(ROOT).load_json("schemas/intent-model-output.schema.json") + with pytest.raises(AXIntentError, match="ax-local"): + classify_with_local_transformers( + hr_input="WRK-001 여권 사본 받아줘", + system_prompt="JSON만 출력", + output_schema=schema, + ) + + +def test_versioned_ax_prompt_is_inside_project() -> None: + path, prompt = load_ax_prompt(ROOT) + assert path.is_relative_to(ROOT) + assert "DOCUMENT_REQUEST" in prompt + assert "OUT_OF_SCOPE" in prompt + assert "JSON 객체 하나만 출력" in prompt