From b7e3e17bdbe00c0f30de76425ea5a1537e176056 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 08:51:08 +0900 Subject: [PATCH 1/9] chore(repo): initialize monorepo collaboration workflow Refs #1 --- .editorconfig | 15 ++++++ .github/ISSUE_TEMPLATE/bug-report.yml | 31 ++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++++ .github/ISSUE_TEMPLATE/knowledge-change.yml | 53 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/model-experiment.yml | 42 ++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 27 +++++++++++ .gitignore | 18 +++++++ CONTRIBUTING.md | 42 ++++++++++++++++ Makefile | 18 +++++++ README.md | 41 +++++++++++++++- pyproject.toml | 2 + 11 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/knowledge-change.yml create mode 100644 .github/ISSUE_TEMPLATE/model-experiment.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 pyproject.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..64d3bf0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..2f11fbb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,31 @@ +name: Bug report +description: Knowledge 검증·CLI·Agent 연동 오류를 신고합니다. +title: "[Bug] " +labels: ["bug"] +body: + - type: textarea + id: problem + attributes: + label: 문제 + description: 실제 결과와 기대 결과를 함께 적습니다. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: 재현 절차 + placeholder: 명령어, 입력 파일, Workflow ID + validations: + required: true + - type: input + id: version + attributes: + label: Context Pack 버전 + placeholder: 0.1.0 + validations: + required: true + - type: textarea + id: logs + attributes: + label: 오류 로그 + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e2994a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: EPS 공식 안내 + url: https://www.eps.go.kr + about: E-9 고용허가제의 최신 공식 절차를 확인합니다. + - name: 하이코리아 + url: https://www.hikorea.go.kr + about: 체류 관련 최신 공식 안내를 확인합니다. diff --git a/.github/ISSUE_TEMPLATE/knowledge-change.yml b/.github/ISSUE_TEMPLATE/knowledge-change.yml new file mode 100644 index 0000000..bbc8051 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/knowledge-change.yml @@ -0,0 +1,53 @@ +name: Knowledge 변경 +description: 공식자료·Workflow·체크리스트·Context Pack 변경을 제안합니다. +title: "[Knowledge] " +labels: ["documentation"] +body: + - type: dropdown + id: change_type + attributes: + label: 변경 유형 + options: + - 공식 출처 추가·갱신 + - Workflow 추가·수정 + - 필수 Slot·체크리스트 변경 + - 모호표현·Guardrail 변경 + - 다국어·쉬운 한국어 규칙 변경 + validations: + required: true + - type: input + id: affected + attributes: + label: 영향받는 ID + description: 예) WF-STY-001, DOCUMENT_REQUEST + validations: + required: true + - type: input + id: source + attributes: + label: 공식 출처 URL + description: 출처가 없는 팀 정책이면 그 사실을 명시합니다. + validations: + required: true + - type: input + id: effective_date + attributes: + label: 시행일 또는 확인일 + placeholder: YYYY-MM-DD + validations: + required: true + - type: textarea + id: proposal + attributes: + label: 변경 내용과 근거 + validations: + required: true + - type: checkboxes + id: safety + attributes: + label: 확인 + options: + - label: 개인정보 또는 원본 신분서류를 포함하지 않았습니다. + required: true + - label: 기관 제출을 자동화하거나 법적 결론을 생성하지 않습니다. + required: true diff --git a/.github/ISSUE_TEMPLATE/model-experiment.yml b/.github/ISSUE_TEMPLATE/model-experiment.yml new file mode 100644 index 0000000..f9a10af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/model-experiment.yml @@ -0,0 +1,42 @@ +name: Model 실험 +description: Intent·Slot·응답 분류 모델의 비교 실험을 제안합니다. +title: "[Experiment] " +labels: ["enhancement"] +body: + - type: input + id: objective + attributes: + label: 실험 목표 + placeholder: 예) Intent Macro-F1 baseline 비교 + validations: + required: true + - type: textarea + id: dataset + attributes: + label: 데이터와 분할 방식 + description: 출처, 건수, 라벨 분포, train/validation/test 분리 기준을 적습니다. + validations: + required: true + - type: textarea + id: baselines + attributes: + label: 비교 모델 + placeholder: TF-IDF + Linear SVM / KoELECTRA / LLM zero-shot + validations: + required: true + - type: textarea + id: metrics + attributes: + label: 평가 지표와 합격 기준 + placeholder: Macro-F1, 클래스별 Recall, 복합요청 Exact Match + validations: + required: true + - type: checkboxes + id: leakage + attributes: + label: 데이터 안전성 + options: + - label: 평가 데이터를 프롬프트 예시나 학습 데이터로 사용하지 않습니다. + required: true + - label: 개인정보와 기업 식별정보가 제거되었습니다. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3c99640 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## 변경 내용 + +- + +## 변경 이유 + +- + +## 검증 + +- [ ] `make validate` +- [ ] `make test` +- [ ] 공식 출처와 확인일 검토 +- [ ] 개인정보·민감정보 미포함 확인 + +## 데이터 변경 + +| 항목 | 내용 | +| --- | --- | +| 추가/수정/삭제 건수 | 해당 없음 | +| 영향받는 Intent/Workflow | 해당 없음 | +| 출처 | 해당 없음 | +| 교차 검수자 | 해당 없음 | + +## 영향 및 제한사항 + +- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2259d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +.idea/ +.vscode/ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.env +.env.* +!.env.example +local-data/ +*.pem +*.key diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2b32bf6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +## Branch + +`/` 형식을 사용합니다. + +- `feat/worker-document-workflow` +- `data/add-payroll-gold-cases` +- `fix/relative-date-validation` +- 자동화 도구가 만드는 브랜치는 `agent/` 사용 + +## Commit + +Conventional Commits를 따릅니다. + +| Type | 사용 시점 | +| --- | --- | +| `feat` | 사용자 또는 Agent 기능 추가 | +| `fix` | 동작 오류 수정 | +| `data` | Context Pack·라벨·출처 데이터 변경 | +| `docs` | 문서만 변경 | +| `test` | 테스트 추가·수정 | +| `refactor` | 동작 변경 없는 구조 개선 | +| `chore` | 도구·설정·의존성 변경 | + +예시: `feat(knowledge): bootstrap validated workflow context pack` + +## Knowledge 변경 규칙 + +1. 공식 절차 변경은 `source_id`, URL, 검증일을 함께 갱신 +2. 법령·행정 지식은 한 명 작성 후 다른 팀원 한 명 이상 검수 +3. 인터뷰 기반 문장은 개인·회사 식별정보를 제거하고 `INTERVIEW_DERIVED_ANON`으로 표시 +4. 합성 문장은 실제 사례로 표현하지 않고 `TEAM_SYNTHETIC`으로 표시 +5. 평가 데이터는 학습·프롬프트 예시 데이터와 분리 +6. `make check` 통과 후 Pull Request 생성 + +## Pull Request + +- 한 PR은 하나의 Workflow 또는 하나의 기술 목적에 집중 +- 데이터 변경 시 추가·수정·삭제 건수와 라벨 분포 기재 +- 공식자료 변경 시 출처와 확인일 기재 +- 개인정보·원본 인터뷰·원본 신분서류 업로드 금지 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f2a6340 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: venv install validate test check + +PYTHON ?= .venv/bin/python +BOOTSTRAP_PYTHON ?= python3.11 + +venv: + $(BOOTSTRAP_PYTHON) -m venv .venv + +install: + $(PYTHON) -m pip install -e "./fowoco-knowledge[dev]" + +validate: + $(PYTHON) -m fowoco_knowledge validate + +test: + $(PYTHON) -m pytest fowoco-knowledge/tests + +check: validate test diff --git a/README.md b/README.md index d959bb3..bdbf6f7 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# fowoco \ No newline at end of file +# FOWOCO + +E-9 외국인근로자를 고용한 사업장의 반복 HR·행정업무를 구조화하고, +담당자가 다음 행동을 빠뜨리지 않도록 지원하는 업무보조 플랫폼입니다. + +이 저장소는 서비스 구성요소를 하나의 저장소에서 관리하는 모노레포입니다. +현재는 Agent가 공통으로 참조할 업무 지식 패키지부터 구축합니다. + +## Packages + +| 디렉토리 | 역할 | 상태 | +| --- | --- | --- | +| `fowoco-knowledge` | Intent·Domain·Workflow·공식 출처·검증 데이터 | 개발 중 | +| `fowoco-client` | HR 웹과 근로자 모바일 웹 | 예정 | +| `fowoco-server` | 인증·업무카드·문서·알림 API | 예정 | +| `fowoco-ai` | 분류·추출·Agent 오케스트레이션 | 예정 | +| `fowoco-infra` | 배포·관측·보안 설정 | 예정 | + +## Quick start + +```bash +python3.11 -m venv .venv +make install +make validate +make test +``` + +자세한 사용법은 [`fowoco-knowledge/README.md`](fowoco-knowledge/README.md)를 참고합니다. + +## 원칙 + +- 법정기한 계산, 필수값 확인, 상태 전이는 검증 가능한 규칙으로 처리 +- AI는 자연어 분류·정보 추출·모호성 탐지·초안 생성에 사용 +- MVP의 모든 외부 발송과 기관 제출은 HR 담당자 승인 후 수행 +- 개인정보 원문은 학습·품질개선 데이터에 저장하지 않음 +- 공식 절차와 서류 지식은 출처, 검증일, 버전을 함께 관리 + +## 협업 + +변경 전 [`CONTRIBUTING.md`](CONTRIBUTING.md)의 브랜치·커밋·데이터 검수 규칙을 확인합니다. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f40dc53 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.uv.workspace] +members = ["fowoco-knowledge"] From 19377bf07c253f2b4a2cd9b59a2bd4ad417b9832 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 08:51:30 +0900 Subject: [PATCH 2/9] feat(knowledge): define MVP workflow context pack Refs #2 --- fowoco-knowledge/CHANGELOG.md | 8 + fowoco-knowledge/README.md | 82 +++++ fowoco-knowledge/docs/SOURCE_POLICY.md | 34 ++ .../knowledge/ambiguity_patterns.yaml | 44 +++ .../knowledge/document_checklists.yaml | 105 ++++++ fowoco-knowledge/knowledge/domains.yaml | 32 ++ .../knowledge/easy_korean_rules.yaml | 22 ++ .../knowledge/guardrail_rules.yaml | 55 ++++ fowoco-knowledge/knowledge/intents.yaml | 66 ++++ fowoco-knowledge/knowledge/manifest.yaml | 35 ++ .../knowledge/multilingual_templates.yaml | 55 ++++ .../knowledge/official_links.yaml | 142 ++++++++ .../knowledge/required_slots.yaml | 78 +++++ .../knowledge/workflow_catalog.yaml | 308 ++++++++++++++++++ fowoco-knowledge/pyproject.toml | 34 ++ .../schemas/agent-analysis.schema.json | 48 +++ .../schemas/agent-request.schema.json | 33 ++ .../schemas/golden-case.schema.json | 40 +++ .../schemas/workflow-catalog.schema.json | 80 +++++ 19 files changed, 1301 insertions(+) create mode 100644 fowoco-knowledge/CHANGELOG.md create mode 100644 fowoco-knowledge/README.md create mode 100644 fowoco-knowledge/docs/SOURCE_POLICY.md create mode 100644 fowoco-knowledge/knowledge/ambiguity_patterns.yaml create mode 100644 fowoco-knowledge/knowledge/document_checklists.yaml create mode 100644 fowoco-knowledge/knowledge/domains.yaml create mode 100644 fowoco-knowledge/knowledge/easy_korean_rules.yaml create mode 100644 fowoco-knowledge/knowledge/guardrail_rules.yaml create mode 100644 fowoco-knowledge/knowledge/intents.yaml create mode 100644 fowoco-knowledge/knowledge/manifest.yaml create mode 100644 fowoco-knowledge/knowledge/multilingual_templates.yaml create mode 100644 fowoco-knowledge/knowledge/official_links.yaml create mode 100644 fowoco-knowledge/knowledge/required_slots.yaml create mode 100644 fowoco-knowledge/knowledge/workflow_catalog.yaml create mode 100644 fowoco-knowledge/pyproject.toml create mode 100644 fowoco-knowledge/schemas/agent-analysis.schema.json create mode 100644 fowoco-knowledge/schemas/agent-request.schema.json create mode 100644 fowoco-knowledge/schemas/golden-case.schema.json create mode 100644 fowoco-knowledge/schemas/workflow-catalog.schema.json diff --git a/fowoco-knowledge/CHANGELOG.md b/fowoco-knowledge/CHANGELOG.md new file mode 100644 index 0000000..d911e56 --- /dev/null +++ b/fowoco-knowledge/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 0.1.0 - 2026-07-16 + +- FOWOCO 6개 Intent와 9개 Domain 정의 +- MVP Workflow Catalog와 필수 Slot 정책 추가 +- 공식 출처 레지스트리와 문서 체크리스트 추가 +- Seed·평가 데이터, 스키마, 검증 CLI 추가 diff --git a/fowoco-knowledge/README.md b/fowoco-knowledge/README.md new file mode 100644 index 0000000..e3ac1fa --- /dev/null +++ b/fowoco-knowledge/README.md @@ -0,0 +1,82 @@ +# FOWOCO Knowledge + +FOWOCO Agent가 공통으로 참조하는 **버전형 업무 지식 패키지**입니다. +Intent·Domain·필수정보·Workflow·공식 출처·Guardrail과 평가 데이터를 한곳에서 관리합니다. + +이 패키지는 LLM 자체가 아닙니다. Agent가 매 요청마다 같은 업무 기준을 사용하도록 +Context를 제공하고, 서로 맞지 않는 지식 변경을 CI에서 차단하는 역할을 합니다. + +## MVP 범위 + +| 지원 업무 | 대표 Workflow | +| --- | --- | +| 근로자 등록 | 문서 OCR 결과를 등록 초안으로 변환 후 HR 승인 | +| 체류·계약 만료 | 내부 알림일에 업무 생성, 필요자료 확인, 수동 기관 제출 안내 | +| 서류 요청 | 대상·서류·기한·제출처 점검 후 근로자 안내와 제출 추적 | +| 급여 설명 | 전월·당월 명세 차이를 계산하고 설명 초안 생성 | +| 업무·일정 안내 | 시간·장소·대상·행동을 명확히 한 다국어 안내 | +| 고용변동 | 사건정보를 구조화하고 공식 신고 준비 업무 생성 | + +기관 자동 제출, 법적 최종판단, 노무위반 확률예측은 포함하지 않습니다. + +## 구조 + +```text +fowoco-knowledge/ +├── knowledge/ # Agent Context Pack 원본 +├── data/ # Seed와 독립 평가 데이터 +├── schemas/ # 입력·Workflow·라벨 데이터 계약 +├── src/ # 로더, 검증기, CLI +├── tests/ # 교차참조·동작 테스트 +├── examples/ # 실행 가능한 요청 예시 +└── docs/ # 데이터·Agent 연동·출처 정책 +``` + +## 설치와 검증 + +저장소 루트에서 실행합니다. + +```bash +python3.11 -m venv .venv +.venv/bin/python -m pip install -e "./fowoco-knowledge[dev]" +.venv/bin/python -m fowoco_knowledge validate +.venv/bin/python -m pytest fowoco-knowledge/tests +``` + +## CLI 사용 + +```bash +# 지원 Workflow 목록 +.venv/bin/python -m fowoco_knowledge list-workflows + +# Agent에 전달할 Context 묶음 확인 +.venv/bin/python -m fowoco_knowledge compile-context WF-STY-001 + +# 분류·Slot Filling 결과가 Workflow를 실행할 수 있는지 확인 +.venv/bin/python -m fowoco_knowledge check-request \ + fowoco-knowledge/examples/ambiguous_document_request.json +``` + +`check-request`는 자연어 모델을 대신하지 않습니다. 모델이 출력한 Workflow와 Slot이 +업무를 시작하기에 충분한지 규칙으로 검증합니다. + +## Agent 연동 위치 + +```text +자연어/PDF/Excel + -> Intent·Domain·Slot 추론 + -> 이 패키지로 Workflow·필수정보·공식출처 조회 + -> 누락·모호성 검증 + -> HR 업무카드와 안내문 초안 생성 + -> HR 승인 + -> 근로자 응답과 후속 티켓 +``` + +## 데이터 상태 + +- `gold_seed.csv`: 프롬프트·분기 개발용 초기 Seed이며 모델 학습 완료 데이터가 아님 +- `golden_cases.jsonl`: 코드와 모델 평가에만 사용하는 독립 사례 +- 공개데이터: 절차·용어·분포 보조자료이며 FOWOCO Intent의 정답 라벨로 간주하지 않음 +- 실제 운영 로그: 개인정보를 제거하고 별도 승인된 경우에만 Active Learning 후보로 사용 + +세부 기준은 [`docs/DATA_GUIDE.md`](docs/DATA_GUIDE.md)를 확인합니다. diff --git a/fowoco-knowledge/docs/SOURCE_POLICY.md b/fowoco-knowledge/docs/SOURCE_POLICY.md new file mode 100644 index 0000000..1902fd9 --- /dev/null +++ b/fowoco-knowledge/docs/SOURCE_POLICY.md @@ -0,0 +1,34 @@ +# 공식 출처와 갱신 정책 + +## 우선순위 + +1. 현행 법령·정부 부처·공공기관의 최신 원문 +2. EPS·하이코리아·고용24 등 실제 업무 시스템 안내 +3. 공공데이터포털 원본 데이터 +4. 공공 연구기관 보고서와 통계 +5. 인터뷰·커뮤니티는 문제 가설과 표현 수집에만 사용 + +블로그와 커뮤니티 글은 법적 절차의 정답으로 사용하지 않습니다. + +## 사용 구분 + +| 목적 | 허용 자료 | 금지 | +| --- | --- | --- | +| Workflow | 공식 절차·서식·필수서류 | 커뮤니티 경험을 공식절차로 일반화 | +| 모델 학습 | 동의·비식별·검수된 문장 | 원본 인사정보·원본 메신저 대화 | +| 모델 보조 | AIHub·KLUE의 일반 Intent/NER | 공개 라벨을 FOWOCO 라벨로 무검수 치환 | +| 평가 | 분리·고정된 Gold 사례 | 학습 또는 Few-shot 예시로 재사용 | + +## 갱신 + +- 공식 출처는 `source_id`를 유지하고 URL·게시일·검증일을 기록 +- 법령·서식 변경 시 영향받는 Workflow와 체크리스트를 Issue에 명시 +- 두 명 검수 후 Context Pack 버전 증가 +- 과거 버전은 삭제하지 않고 Agent 실행 로그에 사용 버전 기록 + +## 라이선스 + +- AIHub는 데이터셋별 이용조건과 재배포 제한을 확인 +- KLUE NER는 CC BY-SA 4.0 표시와 조건 준수 +- 공공데이터는 각 데이터 페이지의 이용조건과 제3자 권리를 확인 +- 원본 데이터를 이 저장소에 복제하기보다 출처 레지스트리와 변환 스크립트를 우선 관리 diff --git a/fowoco-knowledge/knowledge/ambiguity_patterns.yaml b/fowoco-knowledge/knowledge/ambiguity_patterns.yaml new file mode 100644 index 0000000..cd6d6d0 --- /dev/null +++ b/fowoco-knowledge/knowledge/ambiguity_patterns.yaml @@ -0,0 +1,44 @@ +version: 0.1.0 +patterns: + - id: AMB-TIME-001 + category: TIME + terms: [오늘, 내일, 모레, 이번 주, 다음 주, 이번 달, 다음 달, 오후, 퇴근 전, 출근 전] + severity: medium + resolution: 기준일과 정확한 날짜·시각을 확인하고 ISO 8601로 저장한다. + question_template: 정확한 날짜와 시간을 입력해 주세요. + - id: AMB-PLACE-001 + category: LOCATION + terms: [저쪽, 거기, 이쪽, 현장, 사무실, 작업대] + severity: high + resolution: 건물·라인·설비·선반 등 식별 가능한 위치를 확인한다. + question_template: 어느 건물, 라인 또는 번호의 위치인지 입력해 주세요. + - id: AMB-OBJECT-001 + category: OBJECT + terms: [그 서류, 그거, 이거, 저거, 서류, 상자, 기계] + severity: high + resolution: 문서 코드, 설비 번호 또는 대상물 식별정보를 확인한다. + question_template: 정확한 서류명 또는 대상 번호를 입력해 주세요. + - id: AMB-TARGET-001 + category: TARGET + terms: [그 사람, 외국인 직원, 퇴사하는 사람, 재계약 대상자, 우리 직원] + severity: high + resolution: 내부 worker_id 또는 명시적인 대상 그룹을 선택한다. + question_template: 안내 또는 처리 대상 근로자를 선택해 주세요. + - id: AMB-AMOUNT-001 + category: AMOUNT + terms: [조금, 비슷한 금액, 대충, 일부, 더 들어감, 빠짐] + severity: high + resolution: 금액과 급여 항목을 원본 명세서에서 확인한다. + question_template: 정확한 금액과 급여 항목을 확인해 주세요. + - id: AMB-ACTION-001 + category: ACTION + terms: [처리해줘, 준비해줘, 알아서, 평소처럼, 해줘, 정리해줘] + severity: medium + resolution: 원하는 산출물과 완료 기준을 Workflow 단계로 확인한다. + question_template: 확인, 안내, 서류 작성 중 어떤 결과가 필요한지 선택해 주세요. + +detection_policy: + exact_match_only: false + relative_time_always_requires_normalization: true + model_may_add_patterns: false + unknown_pattern_action: FLAG_FOR_REVIEW diff --git a/fowoco-knowledge/knowledge/document_checklists.yaml b/fowoco-knowledge/knowledge/document_checklists.yaml new file mode 100644 index 0000000..1eaf08c --- /dev/null +++ b/fowoco-knowledge/knowledge/document_checklists.yaml @@ -0,0 +1,105 @@ +version: 0.1.0 +status: draft_requires_domain_review +policy: + source_of_truth: SRC-KEIS-REQUIRED-DOCS + latest_official_confirmation_required: true + checklist_is_legal_advice: false + original_documents_stored_encrypted: true +document_types: + PASSPORT_COPY: + label_ko: 여권 사본 + contains_sensitive_data: true + default_validity_check: expiry_date + ALIEN_REGISTRATION_CARD_COPY: + label_ko: 외국인등록증 앞·뒷면 사본 + contains_sensitive_data: true + default_validity_check: stay_expiry_date + EMPLOYMENT_CONTRACT: + label_ko: 근로계약서 + contains_sensitive_data: true + default_validity_check: contract_period + EMPLOYMENT_PERMIT: + label_ko: 외국인근로자 고용허가서 + contains_sensitive_data: true + default_validity_check: permit_period + EMPLOYMENT_CERTIFICATE: + label_ko: 재직증명서 + contains_sensitive_data: true + default_validity_check: issued_at + APPLICATION_FORM: + label_ko: 해당 민원 신청서 + contains_sensitive_data: true + default_validity_check: form_version + PAYSLIP: + label_ko: 임금명세서 + contains_sensitive_data: true + default_validity_check: pay_period + ATTENDANCE_SUMMARY: + label_ko: 근태 집계표 + contains_sensitive_data: true + default_validity_check: attendance_period + +checklists: + - id: CHK-WRK-REGISTER-001 + workflow_id: WF-WRK-001 + purpose: 근로자 등록 초안 생성 + items: + - document_type: PASSPORT_COPY + requirement: required_for_draft + - document_type: ALIEN_REGISTRATION_CARD_COPY + requirement: conditional_if_issued + - document_type: EMPLOYMENT_CONTRACT + requirement: required_for_draft + - document_type: EMPLOYMENT_PERMIT + requirement: recommended_for_cross_check + hr_confirmation: OCR 값과 원본을 대조한 후 등록한다. + + - id: CHK-STAY-RENEW-001 + workflow_id: WF-STY-001 + purpose: 체류기간 갱신 준비 상태 점검 + items: + - document_type: PASSPORT_COPY + requirement: verify_current_copy + - document_type: ALIEN_REGISTRATION_CARD_COPY + requirement: verify_current_copy + - document_type: EMPLOYMENT_CONTRACT + requirement: verify_contract_period + - document_type: APPLICATION_FORM + requirement: select_current_official_form + official_sources: [SRC-HIKOREA, SRC-KEIS-REQUIRED-DOCS] + hr_confirmation: 체류자격과 개별 사정에 따른 추가서류를 하이코리아에서 최종 확인한다. + + - id: CHK-CONTRACT-RENEW-001 + workflow_id: WF-CON-001 + purpose: 재계약 의사와 계약서 준비 + items: + - document_type: EMPLOYMENT_CONTRACT + requirement: use_current_standard_form + - document_type: EMPLOYMENT_PERMIT + requirement: verify_permit_period + official_sources: [SRC-MOEL-CONTRACT-2026, SRC-EPS-FORMS] + hr_confirmation: 근로조건과 당사자 서명은 HR이 확인한다. + + - id: CHK-PAYROLL-EXPLAIN-001 + workflow_id: WF-PAY-001 + purpose: 지급액 차이 설명 근거 확인 + items: + - document_type: PAYSLIP + requirement: required_current_period + - document_type: PAYSLIP + requirement: optional_comparison_period + - document_type: ATTENDANCE_SUMMARY + requirement: conditional_if_hours_explain_difference + official_sources: [SRC-MOEL-WAGE-CALCULATOR] + hr_confirmation: 설명 초안의 항목·기간·금액을 명세서와 대조한다. + + - id: CHK-EMPLOYMENT-CHANGE-001 + workflow_id: WF-CHG-001 + purpose: 고용변동 사건 사실관계와 신고 준비 + items: + - document_type: APPLICATION_FORM + requirement: select_after_hr_confirms_change_type + - document_type: EMPLOYMENT_CONTRACT + requirement: reference_only + official_sources: [SRC-EPS, SRC-EPS-FORMS] + hr_confirmation: 사건 유형·발생일·신고 필요 여부를 담당 기관 또는 전문가와 확인한다. diff --git a/fowoco-knowledge/knowledge/domains.yaml b/fowoco-knowledge/knowledge/domains.yaml new file mode 100644 index 0000000..10eb0c8 --- /dev/null +++ b/fowoco-knowledge/knowledge/domains.yaml @@ -0,0 +1,32 @@ +version: 0.1.0 +domains: + - id: WORKER_PROFILE + label_ko: 근로자 기본정보 + pii_level: high + - id: STAY + label_ko: 체류기간·체류자격 + pii_level: high + - id: CONTRACT + label_ko: 근로계약 + pii_level: high + - id: DOCUMENT + label_ko: 증빙·제출서류 + pii_level: high + - id: INSURANCE + label_ko: 보험·자격취득 + pii_level: high + - id: PAYROLL + label_ko: 급여·수당·공제 + pii_level: high + - id: ATTENDANCE + label_ko: 근태·결근·지각 + pii_level: medium + - id: WORK_SCHEDULE + label_ko: 근무일정·업무안내 + pii_level: medium + - id: EMPLOYMENT_STATUS + label_ko: 입사·퇴사·고용변동 + pii_level: high + - id: GENERAL_ADMIN + label_ko: 일반 사무·증명서 + pii_level: medium diff --git a/fowoco-knowledge/knowledge/easy_korean_rules.yaml b/fowoco-knowledge/knowledge/easy_korean_rules.yaml new file mode 100644 index 0000000..ec6d0cd --- /dev/null +++ b/fowoco-knowledge/knowledge/easy_korean_rules.yaml @@ -0,0 +1,22 @@ +version: 0.1.0 +rules: + - id: KOR-001 + rule: 한 문장에는 한 가지 행동만 작성한다. + - id: KOR-002 + rule: 날짜는 YYYY년 M월 D일 요일 HH시 mm분 형식으로 쓴다. + - id: KOR-003 + rule: 그 서류, 저쪽, 평소처럼 같은 대명사와 관용표현을 쓰지 않는다. + - id: KOR-004 + rule: 기관명, 서류명, 장소명은 공식 명칭과 쉬운 설명을 함께 쓴다. + - id: KOR-005 + rule: 해야 할 행동, 제출물, 기한, 제출처를 순서대로 쓴다. + - id: KOR-006 + rule: 날짜, 금액, 인원, 문서번호는 생성하지 않고 입력값을 그대로 보존한다. + - id: KOR-007 + rule: 급여·체류·고용변동 안내에는 문의 담당자와 확인 방법을 함께 제공한다. + - id: KOR-008 + rule: 완료 기준이 서류 제출이면 확인 버튼 대신 제출 상태를 사용한다. +validation: + max_sentence_length_chars: 60 + preferred_sentences_per_message: 4 + preserve_slot_types: [datetime, month, money, identifier, document_type] diff --git a/fowoco-knowledge/knowledge/guardrail_rules.yaml b/fowoco-knowledge/knowledge/guardrail_rules.yaml new file mode 100644 index 0000000..c7a2fc2 --- /dev/null +++ b/fowoco-knowledge/knowledge/guardrail_rules.yaml @@ -0,0 +1,55 @@ +version: 0.1.0 +confidence_policy: + high: 0.85 + review: 0.65 + behavior: + - condition: confidence < 0.65 + action: REQUEST_CLASSIFICATION_CONFIRMATION + - condition: 0.65 <= confidence < 0.85 + action: HIGHLIGHT_LOW_CONFIDENCE_FIELDS + - condition: confidence >= 0.85 + action: STANDARD_HR_REVIEW + note: MVP에서는 확신도와 관계없이 외부 발송 전에 HR 승인이 필요하다. +rules: + - id: GRD-001 + applies_to: [ALL] + severity: critical + rule: 날짜·금액·서류명·식별자는 원문 또는 구조화 입력에 없는 값을 생성하지 않는다. + validation: compare_immutable_slots + failure_action: BLOCK_AND_REVIEW + - id: GRD-002 + applies_to: [PAYROLL_EXPLANATION] + severity: critical + rule: 급여 계산과 차액은 코드로 산출하고 LLM은 산출 결과만 설명한다. + validation: require_calculation_trace + failure_action: BLOCK_AND_REVIEW + - id: GRD-003 + applies_to: [EXPIRY_RENEWAL, EMPLOYMENT_CHANGE] + severity: critical + rule: 법적 가능 여부와 신고 의무를 확정하지 않고 최신 공식 출처와 수동 확인 단계를 표시한다. + validation: require_official_source_and_manual_step + failure_action: BLOCK_AND_REVIEW + - id: GRD-004 + applies_to: [ALL] + severity: critical + rule: 기관 사이트 제출, 계약 확정, 급여 지급, 메시지 외부 발송을 자동 실행하지 않는다. + validation: require_human_approval + failure_action: BLOCK + - id: GRD-005 + applies_to: [ALL] + severity: high + rule: 품질개선 로그에서 실명·외국인등록번호·여권번호·전화번호·계좌번호를 제거한다. + validation: pii_scan + failure_action: DROP_FROM_LEARNING_LOG + - id: GRD-006 + applies_to: [ALL] + severity: high + rule: 복합 요청은 후보 업무를 분리하고 HR이 각 업무카드를 승인하거나 삭제한다. + validation: require_multi_intent_split + failure_action: RETURN_CANDIDATES + - id: GRD-007 + applies_to: [ALL] + severity: high + rule: 암호화 문서 원본을 LLM 프롬프트에 직접 넣지 않고 필요한 필드만 마스킹해 전달한다. + validation: document_access_policy + failure_action: BLOCK diff --git a/fowoco-knowledge/knowledge/intents.yaml b/fowoco-knowledge/knowledge/intents.yaml new file mode 100644 index 0000000..215915f --- /dev/null +++ b/fowoco-knowledge/knowledge/intents.yaml @@ -0,0 +1,66 @@ +version: 0.1.0 +intents: + - id: WORKER_ONBOARDING + label_ko: 근로자 등록·정보변경 + description: 신규 근로자 등록 초안을 만들거나 기존 기본정보를 변경한다. + allowed_domains: [WORKER_PROFILE, INSURANCE, DOCUMENT] + examples: + - 새로 입사한 외국인 직원 등록 초안 만들어줘 + - 외국인등록번호 변경 내용을 직원 정보에 반영해줘 + exclusions: + - 근로자에게 서류 제출을 요청하는 일은 DOCUMENT_REQUEST + + - id: EXPIRY_RENEWAL + label_ko: 체류·계약 만료 및 갱신 + description: 체류기간이나 근로계약 만료를 확인하고 갱신 준비 Workflow를 시작한다. + allowed_domains: [STAY, CONTRACT] + examples: + - 다음 달 체류기간이 끝나는 근로자 확인해줘 + - 응웬 씨 계약 연장 준비해줘 + exclusions: + - 기관에 최종 신고하거나 법적 가능 여부를 확정하지 않음 + + - id: DOCUMENT_REQUEST + label_ko: 서류 요청·확인 + description: 근로자 또는 내부 담당자에게 필요한 서류를 요청하고 제출 상태를 추적한다. + allowed_domains: [DOCUMENT, STAY, CONTRACT, INSURANCE, GENERAL_ADMIN] + examples: + - 금요일까지 여권 사본을 총무팀에 제출해달라고 안내해줘 + - 아직 안 낸 서류가 있는지 확인해줘 + exclusions: + - 문서에서 근로자 정보를 추출해 등록하는 일은 WORKER_ONBOARDING + + - id: PAYROLL_EXPLANATION + label_ko: 급여·공제 설명 + description: 명세서와 근태 근거를 비교해 지급액 차이와 항목을 설명한다. + allowed_domains: [PAYROLL, ATTENDANCE] + examples: + - 이번 달 월급이 지난달보다 적은 이유 설명해줘 + - 기숙사비 공제 내용을 베트남어로 안내해줘 + exclusions: + - 급여를 직접 계산하거나 지급액을 확정하지 않음 + + - id: WORK_INSTRUCTION + label_ko: 업무·근무일정 안내 + description: 작업, 근무시간, 제출·연락 절차를 명확한 안내문으로 전달한다. + allowed_domains: [WORK_SCHEDULE, ATTENDANCE, GENERAL_ADMIN] + examples: + - 다음 주부터 주간조 출근시간이 바뀐다고 알려줘 + - 출근이 늦으면 반장에게 연락하라고 안내해줘 + exclusions: + - 생산설비 제어와 작업자 자동배치는 수행하지 않음 + + - id: EMPLOYMENT_CHANGE + label_ko: 고용변동·이탈 후속조치 + description: 퇴사, 무단결근, 이탈 등 사건을 구조화하고 확인·신고 준비 업무를 만든다. + allowed_domains: [EMPLOYMENT_STATUS, ATTENDANCE, CONTRACT] + examples: + - 외국인 근로자가 어제부터 연락 없이 출근하지 않았어 + - 이번 달 퇴사 예정자 후속 업무 만들어줘 + exclusions: + - 이탈 여부나 신고 의무를 AI가 최종 확정하지 않음 + +out_of_scope_label: OUT_OF_SCOPE +multi_intent_policy: + strategy: split_and_confirm + description: 복합 요청은 Intent별 후보 업무카드로 분리하고 HR이 승인·삭제한다. diff --git a/fowoco-knowledge/knowledge/manifest.yaml b/fowoco-knowledge/knowledge/manifest.yaml new file mode 100644 index 0000000..e73e568 --- /dev/null +++ b/fowoco-knowledge/knowledge/manifest.yaml @@ -0,0 +1,35 @@ +pack_id: FOWOCO-KNOWLEDGE +version: 0.1.0 +status: draft +released_at: 2026-07-16 +default_locale: ko-KR +supported_worker_locales: + - vi-VN + - id-ID + - km-KH + - ne-NP + - uz-UZ + - th-TH +input_modes: + - WORKER_MESSAGE + - AGENT_TASK + - INTERNAL_REQUEST +files: + intents: knowledge/intents.yaml + domains: knowledge/domains.yaml + slots: knowledge/required_slots.yaml + ambiguities: knowledge/ambiguity_patterns.yaml + workflows: knowledge/workflow_catalog.yaml + sources: knowledge/official_links.yaml + checklists: knowledge/document_checklists.yaml + easy_korean: knowledge/easy_korean_rules.yaml + multilingual_templates: knowledge/multilingual_templates.yaml + guardrails: knowledge/guardrail_rules.yaml +review_policy: + minimum_reviewers_for_official_knowledge: 2 + all_outbound_messages_require_hr_approval: true + automatic_external_submission: false + personally_identifiable_training_data: false +disclaimer: >- + 이 Context Pack은 HR 업무 준비와 설명을 보조한다. 법률 자문이나 기관의 최종 + 판단을 대체하지 않으며 실제 제출 전 최신 공식자료와 담당자의 확인이 필요하다. diff --git a/fowoco-knowledge/knowledge/multilingual_templates.yaml b/fowoco-knowledge/knowledge/multilingual_templates.yaml new file mode 100644 index 0000000..acdbcdc --- /dev/null +++ b/fowoco-knowledge/knowledge/multilingual_templates.yaml @@ -0,0 +1,55 @@ +version: 0.1.0 +policy: + source_of_truth: easy_korean + supported_locales: [vi-VN, id-ID, km-KH, ne-NP, uz-UZ, th-TH] + preserve_placeholders: true + save_source_and_translation_together: true + human_approval_required: true + fallback: easy_korean_only +templates: + - id: TPL-DOC-REQUEST-001 + workflow_id: WF-DOC-001 + easy_korean: |- + {worker_display_name}님, {document_display_name}이 필요합니다. + {due_at_display}까지 {submission_channel_display}에 제출해 주세요. + 아래 링크에서 제출하거나 질문을 남길 수 있습니다. + required_placeholders: + - worker_display_name + - document_display_name + - due_at_display + - submission_channel_display + - id: TPL-SCHEDULE-001 + workflow_id: WF-INS-001 + easy_korean: |- + {effective_at_display}부터 일정이 바뀝니다. + 해야 할 일: {work_action} + 장소: {work_location_display} + 내용을 확인한 뒤 응답해 주세요. + required_placeholders: + - effective_at_display + - work_action + - work_location_display + - id: TPL-PAYROLL-001 + workflow_id: WF-PAY-001 + easy_korean: |- + {pay_period_display} 급여명세서의 차이를 설명합니다. + 변경 항목: {pay_item_display} + 변경 이유: {calculated_reason} + 금액은 첨부된 급여명세서에서 확인해 주세요. + required_placeholders: + - pay_period_display + - pay_item_display + - calculated_reason +worker_response_options: + - code: ACKNOWLEDGED + label_ko: 확인했습니다 + - code: QUESTION + label_ko: 질문이 있습니다 + - code: NOT_UNDERSTOOD + label_ko: 이해하지 못했습니다 + - code: IN_PROGRESS + label_ko: 준비 중입니다 + - code: SUBMITTED + label_ko: 제출했습니다 + - code: CANNOT_COMPLETE + label_ko: 처리하기 어렵습니다 diff --git a/fowoco-knowledge/knowledge/official_links.yaml b/fowoco-knowledge/knowledge/official_links.yaml new file mode 100644 index 0000000..1431503 --- /dev/null +++ b/fowoco-knowledge/knowledge/official_links.yaml @@ -0,0 +1,142 @@ +version: 0.1.0 +last_verified: 2026-07-16 +sources: + - id: SRC-KEIS-REQUIRED-DOCS + title: 한국고용정보원 외국인고용 관련 민원서식 필요서류 목록 + publisher: 한국고용정보원 + url: https://www.data.go.kr/data/15153513/fileData.do + access: public_download + data_shape: CSV 187행, 신청서명·필요서류명·해당업종·샘플서식 제공 여부 + use_for: [document_checklist_seed, workflow_reference] + use_as_model_label: false + license_note: 공공데이터포털 이용조건과 최신 파일의 메타데이터를 재확인한다. + + - id: SRC-KEIS-ALLOWED-INDUSTRIES + title: 고용허가제 허용업종 및 사업내용 다국어 목록 + publisher: 한국고용정보원 + url: https://www.data.go.kr/data/15153406/fileData.do + access: public_download + data_shape: CSV 847행, 한국어·영어 및 송출국 언어 사업내용 + use_for: [industry_taxonomy, multilingual_terminology] + use_as_model_label: false + license_note: 번역모델 학습셋이 아니라 용어 검증과 업종 표준화에 사용한다. + + - id: SRC-EPS-FORMS + title: EPS 외국인고용 관련 서식자료 + publisher: 고용노동부·한국산업인력공단 + url: https://www.eps.go.kr/eo/FormDataLst.eo + access: public_web + data_shape: 표준근로계약서·고용허가·변동신고 등 서식 목록 + use_for: [form_reference, workflow_reference] + use_as_model_label: false + license_note: 서식 버전과 게시일을 Workflow 배포 전에 확인한다. + + - id: SRC-MOEL-CONTRACT-2026 + title: 2026년 외국인 표준근로계약서 다국어 양식 + publisher: 고용노동부 + url: https://www.moel.go.kr/local/tongyeong/info/dataroom/view.do?bbs_seq=20260100271 + access: public_web + data_shape: 국가별 표준근로계약서 첨부파일 + use_for: [contract_field_reference, multilingual_terminology] + use_as_model_label: false + license_note: 원문 양식을 대체 생성하지 않고 설명문 작성과 필드 검증에 사용한다. + + - id: SRC-EPS-LANGUAGE + title: EPS 외국인근로자용 외국어 문장 DB + publisher: 한국산업인력공단 + url: https://eps.hrdkorea.or.kr/e9/user/language/language.do?method=languageSearch&searchLanguage=01 + access: public_web + data_shape: 업무지시·임금·고용변동 등 15개 언어 문장 + use_for: [easy_korean_reference, multilingual_terminology, retrieval] + use_as_model_label: false + license_note: 크롤링·재배포 전 이용조건을 별도 확인한다. + + - id: SRC-EPS-GLOSSARY + title: EPS 고용허가제 용어사전 + publisher: 고용노동부·한국산업인력공단 + url: https://mainevent.eps.go.kr/eo/VocaDicLstR.eo + access: public_web + data_shape: 고용허가제 핵심용어 76개 + use_for: [terminology, easy_korean_reference] + use_as_model_label: false + license_note: 원문 출처를 표시하고 최신 페이지를 기준으로 사용한다. + + - id: SRC-LAW-OPENAPI + title: 국가법령정보 공동활용 Open API + publisher: 법제처 + url: https://open.law.go.kr/LSO/openApi/guideList.do + access: account_and_api_key + data_shape: 현행법령·조문·시행일·연혁 + use_for: [legal_reference, versioned_retrieval] + use_as_model_label: false + license_note: 법률 자문이 아니라 근거 표시와 변경 감지에 한정한다. + + - id: SRC-HOLIDAY-API + title: 한국천문연구원 특일 정보 Open API + publisher: 한국천문연구원 + url: https://www.data.go.kr/data/15012690/openapi.do + access: api_key + data_shape: 공휴일·기념일·24절기 날짜 + use_for: [business_day_calculation] + use_as_model_label: false + license_note: 법정 제출기한 규칙과 별개로 내부 업무일 계산에만 사용한다. + + - id: SRC-MOEL-WAGE-CALCULATOR + title: 고용노동부 임금명세서 작성 프로그램 + publisher: 고용노동부 + url: https://www.moel.go.kr/wageCal.do + access: public_web + data_shape: 임금항목·공제내역·계산 구조 + use_for: [payroll_schema, synthetic_payslip_generation] + use_as_model_label: false + license_note: FOWOCO는 급여 확정이 아니라 보유 명세서의 항목 차이 설명만 수행한다. + + - id: SRC-HIKOREA + title: 하이코리아 전자민원 안내 + publisher: 법무부 + url: https://www.hikorea.go.kr + access: public_web_and_login + data_shape: 체류민원 안내와 전자민원 + use_for: [stay_workflow_reference, manual_submission_link] + use_as_model_label: false + license_note: 세부 민원 URL과 구비서류는 로그인 후 담당자가 최종 확인한다. + + - id: SRC-EPS + title: 고용허가제 EPS + publisher: 고용노동부·한국산업인력공단 + url: https://www.eps.go.kr + access: public_web_and_login + data_shape: 고용허가·고용변동·사업장 업무 안내 + use_for: [employment_workflow_reference, manual_submission_link] + use_as_model_label: false + license_note: FOWOCO는 EPS 로그인을 대행하거나 자동 제출하지 않는다. + + - id: SRC-AIHUB-CIVIL-PETITION + title: AIHub 민원 업무 자동화 데이터 + publisher: 한국지능정보사회진흥원 + url: https://aihub.or.kr/aihubdata/data/view.do?dataSetSn=619 + access: account_required + data_shape: 민원 원문·생성문·Intent·Entity·담당부서·키워드 약 100만 건 + use_for: [intent_entity_pretraining_reference, label_schema_reference] + use_as_model_label: false + license_note: FOWOCO 도메인과 불일치하므로 직접 라벨 전이 금지. AIHub 개별 이용조건 확인. + + - id: SRC-AIHUB-TIMEX + title: AIHub 시간 표현 탐지 데이터 + publisher: 한국지능정보사회진흥원 + url: https://aihub.or.kr/aihubdata/data/view.do?dataSetSn=71613 + access: account_required + data_shape: 문서 41,111건, TIMEX3·EVENT·TLINK + use_for: [date_time_extraction_reference, auxiliary_training] + use_as_model_label: false + license_note: 기준일을 사용한 날짜 정규화는 FOWOCO가 별도로 구현한다. + + - id: SRC-KLUE-NER + title: KLUE NER + publisher: KLUE + url: https://huggingface.co/datasets/klue/klue/tree/main/ner + access: public_download + data_shape: 학습 21,008건·검증 5,000건의 한국어 개체명 라벨 + use_for: [korean_ner_baseline, token_classification_reference] + use_as_model_label: false + license_note: CC BY-SA 4.0. FOWOCO 전용 서류명·급여항목 Slot은 자체 라벨이 필요하다. diff --git a/fowoco-knowledge/knowledge/required_slots.yaml b/fowoco-knowledge/knowledge/required_slots.yaml new file mode 100644 index 0000000..e201cf7 --- /dev/null +++ b/fowoco-knowledge/knowledge/required_slots.yaml @@ -0,0 +1,78 @@ +version: 0.1.0 +slot_definitions: + worker_id: + type: string + description: 내부 근로자 식별자. 모델 로그에는 실명 대신 사용한다. + worker_name: + type: string + description: HR 검토 화면 표시용 이름. 품질개선 로그에서는 마스킹한다. + document_type: + type: string + description: 요청·확인할 표준 문서 유형 코드 + due_at: + type: datetime + description: 제출 또는 조치 마감시각. 상대 날짜는 기준일로 정규화한다. + submission_channel: + type: string + description: 총무팀 방문, 보안 링크 업로드 등 제출 경로 + effective_at: + type: datetime + description: 일정·계약·변경이 시작되는 시각 + work_location: + type: string + description: 공장·라인·작업대 등 실제 위치 + work_action: + type: string + description: 수행할 구체적인 한 가지 행동 + pay_period: + type: month + description: 설명 대상 급여 귀속월 + pay_item: + type: string + description: 기본급·연장근로수당·공제 등 설명 항목 + comparison_period: + type: month + description: 차이를 비교할 이전 급여 귀속월 + change_type: + type: enum + values: [RESIGNATION, ABSENCE, UNREACHABLE, WORKPLACE_CHANGE, OTHER] + description: 고용변동 후보 유형. HR이 최종 확인한다. + incident_at: + type: datetime + description: 결근·연락두절·퇴사 의사 등 사건 발생시각 + reason: + type: string + description: 조치 또는 설명 사유 + source_document_id: + type: string + description: OCR·급여 비교에 사용한 암호화 문서의 내부 식별자 + +workflow_requirements: + WF-WRK-001: + required: [source_document_id] + resolvable_from_context: [] + WF-STY-001: + required: [worker_id, due_at] + resolvable_from_context: [worker_id, due_at] + WF-CON-001: + required: [worker_id, due_at] + resolvable_from_context: [worker_id, due_at] + WF-DOC-001: + required: [worker_id, document_type, due_at, submission_channel] + resolvable_from_context: [worker_id] + WF-PAY-001: + required: [worker_id, pay_period, source_document_id] + optional: [comparison_period, pay_item] + resolvable_from_context: [worker_id, source_document_id] + WF-INS-001: + required: [worker_id, effective_at, work_action] + optional: [work_location] + resolvable_from_context: [worker_id] + WF-CHG-001: + required: [worker_id, change_type, incident_at] + optional: [reason] + resolvable_from_context: [worker_id] + WF-ADM-001: + required: [worker_id, document_type, due_at] + optional: [submission_channel] + resolvable_from_context: [worker_id] diff --git a/fowoco-knowledge/knowledge/workflow_catalog.yaml b/fowoco-knowledge/knowledge/workflow_catalog.yaml new file mode 100644 index 0000000..38b6291 --- /dev/null +++ b/fowoco-knowledge/knowledge/workflow_catalog.yaml @@ -0,0 +1,308 @@ +version: 0.1.0 +policy: + state_model: [DRAFT, NEEDS_INFO, READY_FOR_REVIEW, APPROVED, IN_PROGRESS, WAITING_WORKER, WAITING_EXTERNAL, COMPLETED, CANCELLED] + all_external_actions_require_human: true + completion_requires_evidence: true + composite_request_strategy: split_and_confirm + +workflows: + - id: WF-WRK-001 + name: 문서 기반 근로자 등록 초안 + intent: WORKER_ONBOARDING + domains: [WORKER_PROFILE, DOCUMENT] + sensitivity: high + supported_input_modes: [AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: file_upload + accepted_types: [application/pdf, image/jpeg, image/png] + required_slots_ref: WF-WRK-001 + source_ids: [SRC-MOEL-CONTRACT-2026, SRC-EPS-FORMS] + steps: + - id: ingest_document + actor: SYSTEM + action: 암호화 저장소에 원본을 저장하고 문서 ID만 발급 + output: source_document_id + - id: extract_fields + actor: AI + action: OCR 결과에서 이름·국적·체류기간·계약기간 후보값 추출 + output: worker_profile_draft + - id: validate_fields + actor: RULE_ENGINE + action: 날짜 형식·필수값·문서 간 불일치 검사 + output: validation_report + - id: review_draft + actor: HR + action: 원본과 후보값 대조 후 수정·승인 + output: approved_worker_profile + gate: required + - id: persist_profile + actor: SYSTEM + action: 승인된 필드만 운영 DB에 저장 + output: worker_id + completion_evidence: [hr_approval_id, worker_id] + + - id: WF-STY-001 + name: 체류기간 만료 사전 준비 + intent: EXPIRY_RENEWAL + domains: [STAY, DOCUMENT] + sensitivity: high + supported_input_modes: [AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: schedule + field: stay_expiry_date + reminder_days: [90, 60, 30, 7] + note: 내부 준비 알림일이며 법정 제출기한을 의미하지 않음 + - type: natural_language + required_slots_ref: WF-STY-001 + source_ids: [SRC-HIKOREA, SRC-KEIS-REQUIRED-DOCS, SRC-HOLIDAY-API] + checklist_id: CHK-STAY-RENEW-001 + steps: + - id: create_task + actor: RULE_ENGINE + action: 근로자·만료일·남은 내부 업무일을 포함한 업무카드 생성 + output: stay_renewal_task + - id: load_current_checklist + actor: WORKFLOW_AGENT + action: 최신 확인 상태인 체크리스트와 공식 링크 조회 + output: preparation_checklist + - id: compare_documents + actor: RULE_ENGINE + action: 보유 문서의 존재·유효기간·검증 상태 비교 + output: missing_or_expired_documents + - id: request_worker_documents + actor: LANGUAGE_AGENT + action: 근로자가 제출해야 할 자료만 쉬운 한국어·모국어 초안으로 생성 + output: worker_notice_draft + gate: hr_approval + - id: manual_external_process + actor: HR + action: 공식 사이트 또는 기관에서 최신 구비서류를 확인하고 직접 처리 + output: external_submission_status + - id: close_task + actor: HR + action: 처리 증빙과 차기 확인일을 등록하고 완료 + output: completed_task + completion_evidence: [hr_approval_id, external_receipt_or_note, next_review_at] + + - id: WF-CON-001 + name: 근로계약 갱신 준비 + intent: EXPIRY_RENEWAL + domains: [CONTRACT, DOCUMENT] + sensitivity: high + supported_input_modes: [WORKER_MESSAGE, AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: schedule + field: contract_end_date + reminder_days: [90, 60, 30, 7] + note: 사업장 내부 준비일 + - type: natural_language + required_slots_ref: WF-CON-001 + source_ids: [SRC-MOEL-CONTRACT-2026, SRC-EPS-FORMS] + checklist_id: CHK-CONTRACT-RENEW-001 + steps: + - id: confirm_company_intent + actor: HR + action: 재계약 검토 대상과 회사 의사 확인 + output: company_intent + - id: confirm_worker_intent + actor: LANGUAGE_AGENT + action: 근로자에게 계속 근무 의사를 묻는 안내 초안 생성 + output: worker_intent_request + gate: hr_approval + - id: prepare_contract + actor: WORKFLOW_AGENT + action: 최신 표준양식 링크와 기존 계약 필드 비교표 제공 + output: contract_preparation_pack + - id: review_and_sign + actor: HR + action: 근로조건 검토와 당사자 서명 진행 + output: signed_contract_record + - id: close_task + actor: HR + action: 계약기간과 후속 외부 절차 상태 갱신 + output: completed_task + completion_evidence: [worker_intent, signed_contract_record, hr_approval_id] + + - id: WF-DOC-001 + name: 근로자 서류 요청 및 제출 추적 + intent: DOCUMENT_REQUEST + domains: [DOCUMENT, STAY, CONTRACT, INSURANCE, GENERAL_ADMIN] + sensitivity: medium + supported_input_modes: [WORKER_MESSAGE, AGENT_TASK] + triggers: + - type: natural_language + - type: missing_document + required_slots_ref: WF-DOC-001 + source_ids: [SRC-KEIS-REQUIRED-DOCS, SRC-EPS-LANGUAGE] + steps: + - id: validate_request + actor: AMBIGUITY_AGENT + action: 대상·정확한 서류명·기한·제출처 확인 + output: normalized_request + - id: generate_notice + actor: LANGUAGE_AGENT + action: 쉬운 한국어와 근로자 언어 안내문 생성 + output: notice_draft + - id: approve_notice + actor: HR + action: 원문·핵심값·번역문 검토 후 발송 승인 + output: approved_notice + gate: required + - id: collect_response + actor: WORKER + action: 확인·질문·준비 중·제출 완료·처리 어려움 중 응답 + output: worker_response + - id: verify_submission + actor: HR + action: 제출 파일의 문서 종류와 가독성 확인 + output: document_verification + - id: close_or_ticket + actor: RESPONSE_TICKET_AGENT + action: 완료 처리 또는 질문·미제출 후속 티켓 생성 + output: task_or_ticket + completion_evidence: [approved_notice_id, verified_document_id] + + - id: WF-PAY-001 + name: 급여명세서 차이 설명 + intent: PAYROLL_EXPLANATION + domains: [PAYROLL, ATTENDANCE] + sensitivity: high + supported_input_modes: [AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: natural_language + - type: payslip_upload + required_slots_ref: WF-PAY-001 + source_ids: [SRC-MOEL-WAGE-CALCULATOR] + checklist_id: CHK-PAYROLL-EXPLAIN-001 + steps: + - id: parse_payslip + actor: AI + action: 명세서에서 귀속월·지급항목·공제항목 후보 추출 + output: payroll_fields + - id: calculate_difference + actor: RULE_ENGINE + action: 항목별 전월 대비 차이를 코드로 계산 + output: calculation_trace + - id: generate_explanation + actor: LANGUAGE_AGENT + action: 계산 결과를 쉬운 한국어와 근로자 언어로 설명 + output: explanation_draft + - id: review_explanation + actor: HR + action: 명세서·근태자료와 설명을 대조하고 승인 + output: approved_explanation + gate: required + - id: collect_question + actor: WORKER + action: 확인 또는 질문 응답 + output: worker_response + - id: escalate_if_needed + actor: RESPONSE_TICKET_AGENT + action: 불일치·체불 주장·법률 질문은 HR 또는 전문가 검토 티켓으로 전환 + output: task_or_ticket + completion_evidence: [calculation_trace_id, hr_approval_id] + + - id: WF-INS-001 + name: 업무·근무일정 안내 + intent: WORK_INSTRUCTION + domains: [WORK_SCHEDULE, ATTENDANCE, GENERAL_ADMIN] + sensitivity: medium + supported_input_modes: [WORKER_MESSAGE, AGENT_TASK] + triggers: + - type: natural_language + required_slots_ref: WF-INS-001 + source_ids: [SRC-EPS-LANGUAGE] + steps: + - id: validate_instruction + actor: AMBIGUITY_AGENT + action: 대상·시작일·행동·위치의 누락과 모호표현 확인 + output: normalized_instruction + - id: generate_notice + actor: LANGUAGE_AGENT + action: 한 문장 한 행동 원칙의 쉬운 한국어·모국어 안내 생성 + output: notice_draft + - id: approve_notice + actor: HR + action: 현장관리자 또는 HR이 내용 승인 + output: approved_notice + gate: required + - id: collect_response + actor: WORKER + action: 확인·질문·이해하지 못함·처리 어려움 응답 + output: worker_response + - id: follow_up + actor: RESPONSE_TICKET_AGENT + action: 질문 또는 미확인 상태를 담당자 티켓으로 연결 + output: task_or_ticket + completion_evidence: [approved_notice_id, acknowledged_or_resolved_response] + + - id: WF-CHG-001 + name: 고용변동 사건 확인 및 신고 준비 + intent: EMPLOYMENT_CHANGE + domains: [EMPLOYMENT_STATUS, ATTENDANCE, CONTRACT] + sensitivity: critical + supported_input_modes: [AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: natural_language + - type: attendance_exception + required_slots_ref: WF-CHG-001 + source_ids: [SRC-EPS, SRC-EPS-FORMS] + checklist_id: CHK-EMPLOYMENT-CHANGE-001 + steps: + - id: capture_incident + actor: INTENT_SLOT_AGENT + action: 대상·사건시각·변동 후보유형·연락상태 구조화 + output: incident_draft + - id: confirm_facts + actor: HR + action: 근태·연락기록·당사자 의사로 사실관계 확인 + output: confirmed_incident + gate: required + - id: load_official_guidance + actor: WORKFLOW_AGENT + action: 최신 EPS 서식과 담당 기관 링크 제공 + output: guidance_pack + - id: create_actions + actor: RULE_ENGINE + action: 근로자 연락과 기관 확인을 별도 업무카드로 생성 + output: action_tasks + - id: manual_report + actor: HR + action: 신고 필요 여부를 확인하고 공식 시스템에서 직접 처리 + output: external_submission_status + - id: close_task + actor: HR + action: 처리 근거와 결과 등록 + output: completed_task + completion_evidence: [confirmed_incident_id, external_receipt_or_manual_note] + + - id: WF-ADM-001 + name: 재직증명서 등 일반 증명서 요청 + intent: DOCUMENT_REQUEST + domains: [GENERAL_ADMIN, DOCUMENT] + sensitivity: medium + supported_input_modes: [AGENT_TASK, INTERNAL_REQUEST] + triggers: + - type: natural_language + - type: worker_request + required_slots_ref: WF-ADM-001 + source_ids: [] + steps: + - id: normalize_request + actor: INTENT_SLOT_AGENT + action: 대상·증명서 종류·필요일·사용목적 후보 추출 + output: certificate_request + - id: prepare_draft + actor: WORKFLOW_AGENT + action: 회사 승인 템플릿으로 문서 초안 생성 + output: certificate_draft + - id: approve_document + actor: HR + action: 인적사항·발급일·직인 처리 검토 + output: approved_certificate + gate: required + - id: deliver_document + actor: HR + action: 회사 정책에 따른 채널로 전달 + output: delivery_record + completion_evidence: [approved_certificate_id, delivery_record] diff --git a/fowoco-knowledge/pyproject.toml b/fowoco-knowledge/pyproject.toml new file mode 100644 index 0000000..51a0354 --- /dev/null +++ b/fowoco-knowledge/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools>=75"] +build-backend = "setuptools.build_meta" + +[project] +name = "fowoco-knowledge" +version = "0.1.0" +description = "Versioned workflow context packs and validation tools for FOWOCO" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "Proprietary" } +dependencies = [ + "jsonschema>=4.23,<5", + "PyYAML>=6.0,<7", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3,<9", + "pytest-cov>=6,<7", +] + +[project.scripts] +fowoco-knowledge = "fowoco_knowledge.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] + +[tool.coverage.run] +source = ["fowoco_knowledge"] diff --git a/fowoco-knowledge/schemas/agent-analysis.schema.json b/fowoco-knowledge/schemas/agent-analysis.schema.json new file mode 100644 index 0000000..5b0941c --- /dev/null +++ b/fowoco-knowledge/schemas/agent-analysis.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fowoco.local/schemas/agent-analysis.schema.json", + "title": "FOWOCO Intent and Slot model output", + "type": "object", + "required": ["request_id", "input_mode", "intent_candidates", "domains", "slots", "model_version"], + "properties": { + "request_id": { "type": "string" }, + "input_mode": { "enum": ["WORKER_MESSAGE", "AGENT_TASK", "INTERNAL_REQUEST"] }, + "intent_candidates": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["intent", "confidence"], + "properties": { + "intent": { "type": "string" }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, + "suggested_workflow_ids": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + } + }, + "domains": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "slots": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["value", "confidence", "source_span"], + "properties": { + "value": {}, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, + "source_span": { "type": ["string", "null"] } + }, + "additionalProperties": false + } + }, + "model_version": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/fowoco-knowledge/schemas/agent-request.schema.json b/fowoco-knowledge/schemas/agent-request.schema.json new file mode 100644 index 0000000..230a32d --- /dev/null +++ b/fowoco-knowledge/schemas/agent-request.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fowoco.local/schemas/agent-request.schema.json", + "title": "FOWOCO classified request", + "type": "object", + "required": ["request_id", "input_mode", "utterance", "workflow_id", "slots"], + "properties": { + "request_id": { "type": "string", "minLength": 1 }, + "reference_at": { "type": "string", "format": "date-time" }, + "input_mode": { "enum": ["WORKER_MESSAGE", "AGENT_TASK", "INTERNAL_REQUEST"] }, + "utterance": { "type": "string", "minLength": 1 }, + "workflow_id": { "type": "string", "pattern": "^WF-[A-Z]{3}-[0-9]{3}$" }, + "intent_candidates": { + "type": "array", + "items": { + "type": "object", + "required": ["intent", "confidence"], + "properties": { + "intent": { "type": "string" }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 } + }, + "additionalProperties": false + } + }, + "domains": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "slots": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/fowoco-knowledge/schemas/golden-case.schema.json b/fowoco-knowledge/schemas/golden-case.schema.json new file mode 100644 index 0000000..a4c4ac8 --- /dev/null +++ b/fowoco-knowledge/schemas/golden-case.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fowoco.local/schemas/golden-case.schema.json", + "title": "FOWOCO independent evaluation case", + "type": "object", + "required": [ + "case_id", + "input_mode", + "utterance", + "expected_intents", + "expected_domains", + "expected_slots", + "expected_missing_slots", + "expected_action" + ], + "properties": { + "case_id": { "type": "string", "pattern": "^EVAL-[0-9]{3}$" }, + "input_mode": { "enum": ["WORKER_MESSAGE", "AGENT_TASK", "INTERNAL_REQUEST"] }, + "utterance": { "type": "string", "minLength": 1 }, + "system_context": { "type": "object" }, + "expected_intents": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "expected_domains": { "type": "array", "items": { "type": "string" } }, + "expected_workflow_ids": { "type": "array", "items": { "type": "string" } }, + "expected_slots": { "type": "object" }, + "expected_missing_slots": { "type": "array", "items": { "type": "string" } }, + "expected_ambiguity_categories": { "type": "array", "items": { "type": "string" } }, + "expected_action": { + "enum": [ + "REQUEST_CLARIFICATION", + "REQUEST_CLASSIFICATION_CONFIRMATION", + "CREATE_DRAFT_TASK", + "REQUIRE_HR_REVIEW", + "SPLIT_AND_CONFIRM", + "OUT_OF_SCOPE" + ] + }, + "provenance": { "enum": ["TEAM_SYNTHETIC", "INTERVIEW_DERIVED_ANON"] } + }, + "additionalProperties": false +} diff --git a/fowoco-knowledge/schemas/workflow-catalog.schema.json b/fowoco-knowledge/schemas/workflow-catalog.schema.json new file mode 100644 index 0000000..430741a --- /dev/null +++ b/fowoco-knowledge/schemas/workflow-catalog.schema.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fowoco.local/schemas/workflow-catalog.schema.json", + "title": "FOWOCO Workflow Catalog", + "type": "object", + "required": ["version", "policy", "workflows"], + "properties": { + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "policy": { "type": "object" }, + "workflows": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "id", + "name", + "intent", + "domains", + "sensitivity", + "supported_input_modes", + "triggers", + "required_slots_ref", + "source_ids", + "steps", + "completion_evidence" + ], + "properties": { + "id": { "type": "string", "pattern": "^WF-[A-Z]{3}-[0-9]{3}$" }, + "name": { "type": "string", "minLength": 2 }, + "intent": { "type": "string" }, + "domains": { + "type": "array", + "minItems": 1, + "items": { "type": "string" }, + "uniqueItems": true + }, + "sensitivity": { "enum": ["low", "medium", "high", "critical"] }, + "supported_input_modes": { + "type": "array", + "minItems": 1, + "items": { "enum": ["WORKER_MESSAGE", "AGENT_TASK", "INTERNAL_REQUEST"] }, + "uniqueItems": true + }, + "triggers": { "type": "array", "minItems": 1 }, + "required_slots_ref": { "type": "string" }, + "source_ids": { + "type": "array", + "items": { "type": "string", "pattern": "^SRC-" }, + "uniqueItems": true + }, + "checklist_id": { "type": "string", "pattern": "^CHK-" }, + "steps": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "actor", "action", "output"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9_]+$" }, + "actor": { "type": "string" }, + "action": { "type": "string", "minLength": 5 }, + "output": { "type": "string" }, + "gate": { "type": "string" } + }, + "additionalProperties": false + } + }, + "completion_evidence": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} From 92a66bed2f6462ebbed392cb38dd73eaec3ad876 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 08:51:54 +0900 Subject: [PATCH 3/9] data(knowledge): add seed and evaluation corpora Refs #4 --- .../data/evaluation/golden_cases.jsonl | 18 +++++ fowoco-knowledge/data/external/README.md | 11 +++ fowoco-knowledge/data/provenance.yaml | 21 ++++++ fowoco-knowledge/data/seed/gold_seed.csv | 41 ++++++++++ fowoco-knowledge/docs/DATA_GUIDE.md | 55 ++++++++++++++ fowoco-knowledge/docs/MODEL_PLAN.md | 74 +++++++++++++++++++ 6 files changed, 220 insertions(+) create mode 100644 fowoco-knowledge/data/evaluation/golden_cases.jsonl create mode 100644 fowoco-knowledge/data/external/README.md create mode 100644 fowoco-knowledge/data/provenance.yaml create mode 100644 fowoco-knowledge/data/seed/gold_seed.csv create mode 100644 fowoco-knowledge/docs/DATA_GUIDE.md create mode 100644 fowoco-knowledge/docs/MODEL_PLAN.md diff --git a/fowoco-knowledge/data/evaluation/golden_cases.jsonl b/fowoco-knowledge/data/evaluation/golden_cases.jsonl new file mode 100644 index 0000000..adffa0d --- /dev/null +++ b/fowoco-knowledge/data/evaluation/golden_cases.jsonl @@ -0,0 +1,18 @@ +{"case_id":"EVAL-001","input_mode":"AGENT_TASK","utterance":"첨부한 입사 서류에서 근로자 정보를 읽어 등록 초안을 만들어줘","system_context":{"selected_document_id":"DOC-EVAL-001"},"expected_intents":["WORKER_ONBOARDING"],"expected_domains":["WORKER_PROFILE","DOCUMENT"],"expected_workflow_ids":["WF-WRK-001"],"expected_slots":{"source_document_id":"DOC-EVAL-001"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"REQUIRE_HR_REVIEW","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-002","input_mode":"INTERNAL_REQUEST","utterance":"등록증이 바뀐 직원 정보를 수정해줘","system_context":{},"expected_intents":["WORKER_ONBOARDING"],"expected_domains":["WORKER_PROFILE","DOCUMENT"],"expected_workflow_ids":["WF-WRK-001"],"expected_slots":{},"expected_missing_slots":["source_document_id"],"expected_ambiguity_categories":["TARGET","OBJECT"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-003","input_mode":"INTERNAL_REQUEST","utterance":"2026년 8월에 체류기간이 끝나는 사람들의 준비 업무를 만들어줘","system_context":{"reference_at":"2026-07-16T09:00:00+09:00"},"expected_intents":["EXPIRY_RENEWAL"],"expected_domains":["STAY"],"expected_workflow_ids":["WF-STY-001"],"expected_slots":{"due_at":"2026-08"},"expected_missing_slots":["worker_id"],"expected_ambiguity_categories":["TARGET"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-004","input_mode":"AGENT_TASK","utterance":"WRK-EVAL-004의 2026년 8월 31일 계약 종료 전에 재계약 준비를 시작해줘","system_context":{},"expected_intents":["EXPIRY_RENEWAL"],"expected_domains":["CONTRACT"],"expected_workflow_ids":["WF-CON-001"],"expected_slots":{"worker_id":"WRK-EVAL-004","due_at":"2026-08-31T23:59:00+09:00"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"REQUIRE_HR_REVIEW","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-005","input_mode":"WORKER_MESSAGE","utterance":"WRK-EVAL-005에게 2026년 7월 28일 17시까지 외국인등록증 사본을 보안 링크로 올려 달라고 안내해줘","system_context":{},"expected_intents":["DOCUMENT_REQUEST"],"expected_domains":["DOCUMENT"],"expected_workflow_ids":["WF-DOC-001"],"expected_slots":{"worker_id":"WRK-EVAL-005","document_type":"ALIEN_REGISTRATION_CARD_COPY","due_at":"2026-07-28T17:00:00+09:00","submission_channel":"SECURE_LINK"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"CREATE_DRAFT_TASK","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-006","input_mode":"WORKER_MESSAGE","utterance":"다음 주에 그 서류를 사무실로 가져오라고 전해줘","system_context":{"reference_at":"2026-07-16T09:00:00+09:00"},"expected_intents":["DOCUMENT_REQUEST"],"expected_domains":["DOCUMENT"],"expected_workflow_ids":["WF-DOC-001"],"expected_slots":{"submission_channel":"OFFICE"},"expected_missing_slots":["worker_id","document_type","due_at"],"expected_ambiguity_categories":["TIME","OBJECT","TARGET"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-007","input_mode":"AGENT_TASK","utterance":"WRK-EVAL-007의 6월 명세서가 5월보다 줄어든 항목을 설명해줘","system_context":{"selected_document_id":"DOC-EVAL-PAY-007"},"expected_intents":["PAYROLL_EXPLANATION"],"expected_domains":["PAYROLL"],"expected_workflow_ids":["WF-PAY-001"],"expected_slots":{"worker_id":"WRK-EVAL-007","pay_period":"2026-06","comparison_period":"2026-05","source_document_id":"DOC-EVAL-PAY-007"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"REQUIRE_HR_REVIEW","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-008","input_mode":"AGENT_TASK","utterance":"이번에 돈이 조금 빠진 이유를 알려줘","system_context":{},"expected_intents":["PAYROLL_EXPLANATION"],"expected_domains":["PAYROLL"],"expected_workflow_ids":["WF-PAY-001"],"expected_slots":{},"expected_missing_slots":["worker_id","pay_period","source_document_id"],"expected_ambiguity_categories":["TIME","AMOUNT","TARGET"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-009","input_mode":"WORKER_MESSAGE","utterance":"WRK-EVAL-009에게 2026년 7월 21일부터 B동 2번 라인으로 출근하라고 안내해줘","system_context":{},"expected_intents":["WORK_INSTRUCTION"],"expected_domains":["WORK_SCHEDULE"],"expected_workflow_ids":["WF-INS-001"],"expected_slots":{"worker_id":"WRK-EVAL-009","effective_at":"2026-07-21T00:00:00+09:00","work_location":"B동 2번 라인","work_action":"B동 2번 라인으로 출근"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"CREATE_DRAFT_TASK","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-010","input_mode":"WORKER_MESSAGE","utterance":"내일부터 평소처럼 저쪽에서 일하라고 해줘","system_context":{"reference_at":"2026-07-16T09:00:00+09:00"},"expected_intents":["WORK_INSTRUCTION"],"expected_domains":["WORK_SCHEDULE"],"expected_workflow_ids":["WF-INS-001"],"expected_slots":{},"expected_missing_slots":["worker_id","effective_at","work_action"],"expected_ambiguity_categories":["TIME","LOCATION","ACTION","TARGET"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-011","input_mode":"INTERNAL_REQUEST","utterance":"WRK-EVAL-011이 2026년 7월 15일부터 연락되지 않고 결근 중이야. 확인 업무 만들어줘","system_context":{},"expected_intents":["EMPLOYMENT_CHANGE"],"expected_domains":["EMPLOYMENT_STATUS","ATTENDANCE"],"expected_workflow_ids":["WF-CHG-001"],"expected_slots":{"worker_id":"WRK-EVAL-011","change_type":"UNREACHABLE","incident_at":"2026-07-15T00:00:00+09:00"},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"REQUIRE_HR_REVIEW","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-012","input_mode":"AGENT_TASK","utterance":"퇴사하겠다는 사람이 있어. 알아서 신고해줘","system_context":{},"expected_intents":["EMPLOYMENT_CHANGE"],"expected_domains":["EMPLOYMENT_STATUS"],"expected_workflow_ids":["WF-CHG-001"],"expected_slots":{"change_type":"RESIGNATION"},"expected_missing_slots":["worker_id","incident_at"],"expected_ambiguity_categories":["TARGET","ACTION"],"expected_action":"REQUEST_CLARIFICATION","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-013","input_mode":"AGENT_TASK","utterance":"WRK-EVAL-013 체류연장 준비하면서 7월 급여에서 공제된 항목도 설명해줘","system_context":{"selected_document_id":"DOC-EVAL-PAY-013"},"expected_intents":["EXPIRY_RENEWAL","PAYROLL_EXPLANATION"],"expected_domains":["STAY","PAYROLL"],"expected_workflow_ids":["WF-STY-001","WF-PAY-001"],"expected_slots":{"worker_id":"WRK-EVAL-013","pay_period":"2026-07","source_document_id":"DOC-EVAL-PAY-013"},"expected_missing_slots":["due_at"],"expected_ambiguity_categories":[],"expected_action":"SPLIT_AND_CONFIRM","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-014","input_mode":"WORKER_MESSAGE","utterance":"WRK-EVAL-014에게 여권 사본을 요청하고 7월 22일부터 야간조라고 안내해줘","system_context":{},"expected_intents":["DOCUMENT_REQUEST","WORK_INSTRUCTION"],"expected_domains":["DOCUMENT","WORK_SCHEDULE"],"expected_workflow_ids":["WF-DOC-001","WF-INS-001"],"expected_slots":{"worker_id":"WRK-EVAL-014","document_type":"PASSPORT_COPY","effective_at":"2026-07-22T00:00:00+09:00","work_action":"야간조 근무"},"expected_missing_slots":["due_at","submission_channel"],"expected_ambiguity_categories":[],"expected_action":"SPLIT_AND_CONFIRM","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-015","input_mode":"AGENT_TASK","utterance":"첨부 서류로 직원을 등록하고 계약 종료일 알림도 설정해줘","system_context":{"selected_document_id":"DOC-EVAL-015"},"expected_intents":["WORKER_ONBOARDING","EXPIRY_RENEWAL"],"expected_domains":["WORKER_PROFILE","CONTRACT"],"expected_workflow_ids":["WF-WRK-001","WF-CON-001"],"expected_slots":{"source_document_id":"DOC-EVAL-015"},"expected_missing_slots":["worker_id","due_at"],"expected_ambiguity_categories":[],"expected_action":"SPLIT_AND_CONFIRM","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-016","input_mode":"INTERNAL_REQUEST","utterance":"다음 달 프레스 라인의 생산량을 예측해줘","system_context":{},"expected_intents":["OUT_OF_SCOPE"],"expected_domains":[],"expected_workflow_ids":[],"expected_slots":{},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"OUT_OF_SCOPE","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-017","input_mode":"AGENT_TASK","utterance":"이 계약 해지가 법적으로 유효한지 최종 판단해줘","system_context":{},"expected_intents":["OUT_OF_SCOPE"],"expected_domains":["CONTRACT"],"expected_workflow_ids":[],"expected_slots":{},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"OUT_OF_SCOPE","provenance":"TEAM_SYNTHETIC"} +{"case_id":"EVAL-018","input_mode":"AGENT_TASK","utterance":"근로자 여권번호 전체를 AI 학습 데이터로 저장해줘","system_context":{},"expected_intents":["OUT_OF_SCOPE"],"expected_domains":["DOCUMENT"],"expected_workflow_ids":[],"expected_slots":{},"expected_missing_slots":[],"expected_ambiguity_categories":[],"expected_action":"OUT_OF_SCOPE","provenance":"TEAM_SYNTHETIC"} diff --git a/fowoco-knowledge/data/external/README.md b/fowoco-knowledge/data/external/README.md new file mode 100644 index 0000000..040dc7a --- /dev/null +++ b/fowoco-knowledge/data/external/README.md @@ -0,0 +1,11 @@ +# External data + +공식 원본 데이터는 이 디렉토리에 커밋하지 않습니다. + +1. `knowledge/official_links.yaml`의 출처에서 원본 다운로드 +2. 라이선스·게시일·행 수 확인 +3. `local-data/`에서 비식별·정규화 작업 +4. 필요한 코드·용어·출처 ID만 Context Pack에 반영 +5. Knowledge 변경 Issue와 Pull Request에 확인일·변환 규칙 기록 + +이 방식은 원본 데이터 재배포와 오래된 스냅샷의 무분별한 사용을 방지합니다. diff --git a/fowoco-knowledge/data/provenance.yaml b/fowoco-knowledge/data/provenance.yaml new file mode 100644 index 0000000..fadbe8a --- /dev/null +++ b/fowoco-knowledge/data/provenance.yaml @@ -0,0 +1,21 @@ +version: 0.1.0 +sources: + TEAM_SYNTHETIC: + description: 팀이 FOWOCO Workflow와 라벨 기준 검증을 위해 작성한 합성 문장 + may_train: true + requirements: + - 실제 기업 사례로 표현하지 않음 + - 두 명 교차 검수 후 Gold 승격 + - 개인정보와 실제 식별자 금지 + INTERVIEW_DERIVED_ANON: + description: 익명 인터뷰에서 확인된 업무 패턴을 식별 불가능한 문장으로 재작성 + may_train: conditional + requirements: + - 원문 대화와 회사명을 저장하지 않음 + - 인터뷰 동의 범위를 확인 + - 구체적 개인·기업 정보 제거 +review_statuses: + - DRAFT + - REVIEWED_ONCE + - GOLD + - REJECTED diff --git a/fowoco-knowledge/data/seed/gold_seed.csv b/fowoco-knowledge/data/seed/gold_seed.csv new file mode 100644 index 0000000..559022f --- /dev/null +++ b/fowoco-knowledge/data/seed/gold_seed.csv @@ -0,0 +1,41 @@ +request_id,source,input_mode,hr_utterance,system_context,intents,domains,workflow_ids,slots_json,missing_slots,ambiguities,sensitivity,next_action,expected_output,review_status +SEED-001,TEAM_SYNTHETIC,AGENT_TASK,새로 입사한 외국인 직원 등록 초안 만들어줘,입사서류 PDF 선택됨,WORKER_ONBOARDING,WORKER_PROFILE|DOCUMENT,WF-WRK-001,"{""source_document_id"":""DOC-DEMO-001""}",,,high,REQUIRE_HR_REVIEW,근로자 등록 초안,DRAFT +SEED-002,TEAM_SYNTHETIC,AGENT_TASK,이 외국인등록증으로 직원 정보 등록해줘,이미지 파일 선택됨,WORKER_ONBOARDING,WORKER_PROFILE|DOCUMENT,WF-WRK-001,"{""source_document_id"":""DOC-DEMO-002""}",,,high,REQUIRE_HR_REVIEW,OCR 필드와 원본 대조 화면,DRAFT +SEED-003,TEAM_SYNTHETIC,INTERNAL_REQUEST,외국인등록번호 변경 내용을 근로자 정보에 반영해줘,대상 근로자 미선택,WORKER_ONBOARDING,WORKER_PROFILE,WF-WRK-001,"{}",source_document_id,TARGET|OBJECT,high,REQUEST_CLARIFICATION,변경 문서 업로드 요청,DRAFT +SEED-004,TEAM_SYNTHETIC,AGENT_TASK,계약서 보고 새 직원 초안 등록해줘,계약서 PDF 선택됨,WORKER_ONBOARDING,WORKER_PROFILE|CONTRACT,WF-WRK-001,"{""source_document_id"":""DOC-DEMO-004""}",,,high,REQUIRE_HR_REVIEW,계약 필드 등록 초안,DRAFT +SEED-005,INTERVIEW_DERIVED_ANON,INTERNAL_REQUEST,신규 외국인 직원 보험 등록에 필요한 정보 정리해줘,첨부 문서 없음,WORKER_ONBOARDING,INSURANCE|DOCUMENT,WF-WRK-001,"{}",source_document_id,ACTION,high,REQUEST_CLARIFICATION,필요 정보와 문서 업로드 안내,DRAFT +SEED-006,TEAM_SYNTHETIC,AGENT_TASK,다음 달 체류기간이 끝나는 근로자 확인해줘,기준일 2026-07-16,EXPIRY_RENEWAL,STAY,WF-STY-001,"{}",worker_id|due_at,TIME|TARGET,high,REQUEST_CLARIFICATION,대상 후보 목록,DRAFT +SEED-007,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-001 체류연장 준비해줘,근로자 만료일 DB 조회 가능,EXPIRY_RENEWAL,STAY|DOCUMENT,WF-STY-001,"{""worker_id"":""WRK-DEMO-001""}",due_at,ACTION,high,REQUIRE_HR_REVIEW,체류연장 준비 업무카드,DRAFT +SEED-008,TEAM_SYNTHETIC,INTERNAL_REQUEST,7월 31일 계약 만료자 재계약 준비해줘,대상자 목록 조회 가능,EXPIRY_RENEWAL,CONTRACT,WF-CON-001,"{""due_at"":""2026-07-31T23:59:00+09:00""}",worker_id,ACTION,high,REQUEST_CLARIFICATION,대상자 선택 화면,DRAFT +SEED-009,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-002 계약을 2026년 9월 1일부터 연장할 준비를 해줘,계약종료일 DB 존재,EXPIRY_RENEWAL,CONTRACT,WF-CON-001,"{""worker_id"":""WRK-DEMO-002"",""due_at"":""2026-08-31T23:59:00+09:00"",""effective_at"":""2026-09-01T00:00:00+09:00""}",,ACTION,high,REQUIRE_HR_REVIEW,재계약 의사확인 업무카드,DRAFT +SEED-010,INTERVIEW_DERIVED_ANON,INTERNAL_REQUEST,이번 주에 신고나 연장할 사람이 있는지 확인해줘,만료일 DB 존재,EXPIRY_RENEWAL,STAY|CONTRACT,WF-STY-001|WF-CON-001,"{}",worker_id|due_at,TIME|TARGET,high,SPLIT_AND_CONFIRM,체류·계약 후보 업무카드,DRAFT +SEED-011,TEAM_SYNTHETIC,WORKER_MESSAGE,WRK-DEMO-003에게 2026년 7월 24일 오후 5시까지 여권 사본을 총무팀에 제출해달라고 안내해줘,근로자 언어 vi-VN,DOCUMENT_REQUEST,DOCUMENT,WF-DOC-001,"{""worker_id"":""WRK-DEMO-003"",""document_type"":""PASSPORT_COPY"",""due_at"":""2026-07-24T17:00:00+09:00"",""submission_channel"":""HR_OFFICE""}",,,medium,CREATE_DRAFT_TASK,다국어 서류 요청 초안,DRAFT +SEED-012,TEAM_SYNTHETIC,WORKER_MESSAGE,금요일까지 외국인등록증 사본 내라고 해줘,대상 근로자 미선택,DOCUMENT_REQUEST,DOCUMENT,WF-DOC-001,"{""document_type"":""ALIEN_REGISTRATION_CARD_COPY""}",worker_id|due_at|submission_channel,TIME|TARGET,medium,REQUEST_CLARIFICATION,대상·기한·제출처 질문,DRAFT +SEED-013,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-004가 아직 안 낸 서류 확인해줘,문서상태 DB 존재,DOCUMENT_REQUEST,DOCUMENT,WF-DOC-001,"{""worker_id"":""WRK-DEMO-004""}",document_type|due_at|submission_channel,OBJECT,medium,REQUEST_CLARIFICATION,미제출 서류 후보 목록,DRAFT +SEED-014,INTERVIEW_DERIVED_ANON,WORKER_MESSAGE,재직증명서를 은행에 낼 수 있게 발급 준비해줘,근로자 선택됨,DOCUMENT_REQUEST,GENERAL_ADMIN|DOCUMENT,WF-ADM-001,"{""worker_id"":""WRK-DEMO-005"",""document_type"":""EMPLOYMENT_CERTIFICATE""}",due_at,,medium,REQUEST_CLARIFICATION,필요일 확인 질문,DRAFT +SEED-015,TEAM_SYNTHETIC,WORKER_MESSAGE,병원 갔다 올 때 그 서류 떼 오라고 알려줘,대상 미선택,DOCUMENT_REQUEST,DOCUMENT,WF-DOC-001,"{}",worker_id|document_type|due_at|submission_channel,OBJECT|TARGET|TIME,medium,REQUEST_CLARIFICATION,정확한 서류·대상·기한 질문,DRAFT +SEED-016,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-006 이번 달 월급이 지난달보다 적은 이유 설명해줘,당월 급여명세서 선택됨,PAYROLL_EXPLANATION,PAYROLL|ATTENDANCE,WF-PAY-001,"{""worker_id"":""WRK-DEMO-006"",""pay_period"":""2026-06"",""comparison_period"":""2026-05"",""source_document_id"":""DOC-PAY-006""}",,,high,REQUIRE_HR_REVIEW,항목별 차이 계산과 설명 초안,DRAFT +SEED-017,TEAM_SYNTHETIC,AGENT_TASK,이번 급여에 야간수당이 왜 없는지 확인해줘,근로자·명세서 미선택,PAYROLL_EXPLANATION,PAYROLL,WF-PAY-001,"{""pay_item"":""NIGHT_WORK_ALLOWANCE""}",worker_id|pay_period|source_document_id,TIME|TARGET,high,REQUEST_CLARIFICATION,대상·귀속월·명세서 선택 요청,DRAFT +SEED-018,TEAM_SYNTHETIC,WORKER_MESSAGE,WRK-DEMO-007에게 7월 급여에서 기숙사비 15만원이 공제됐다고 설명해줘,7월 명세서 선택됨,PAYROLL_EXPLANATION,PAYROLL,WF-PAY-001,"{""worker_id"":""WRK-DEMO-007"",""pay_period"":""2026-07"",""pay_item"":""DORMITORY_DEDUCTION"",""source_document_id"":""DOC-PAY-007""}",,,high,REQUIRE_HR_REVIEW,공제 근거 설명 초안,DRAFT +SEED-019,INTERVIEW_DERIVED_ANON,AGENT_TASK,월급에서 만원이 덜 들어왔다는데 이유 찾아줘,대상·명세서 미선택,PAYROLL_EXPLANATION,PAYROLL,WF-PAY-001,"{}",worker_id|pay_period|source_document_id,AMOUNT|TARGET,high,REQUEST_CLARIFICATION,근로자와 급여명세서 선택 요청,DRAFT +SEED-020,TEAM_SYNTHETIC,WORKER_MESSAGE,이번 달 급여는 회사 사정으로 다음 달에 줄 거라고 알려줘,근거 문서 없음,PAYROLL_EXPLANATION,PAYROLL,WF-PAY-001,"{}",worker_id|pay_period|source_document_id,TIME|TARGET,critical,REQUIRE_HR_REVIEW,위험표현 차단과 전문가 확인 티켓,DRAFT +SEED-021,TEAM_SYNTHETIC,WORKER_MESSAGE,WRK-DEMO-008에게 2026년 7월 20일부터 오전 7시 30분에 출근하라고 안내해줘,근로자 언어 id-ID,WORK_INSTRUCTION,WORK_SCHEDULE,WF-INS-001,"{""worker_id"":""WRK-DEMO-008"",""effective_at"":""2026-07-20T07:30:00+09:00"",""work_action"":""오전 7시 30분까지 출근""}",,,medium,CREATE_DRAFT_TASK,근무시간 변경 안내 초안,DRAFT +SEED-022,TEAM_SYNTHETIC,WORKER_MESSAGE,오늘 오후에 저쪽 팔레트를 출하장으로 옮기라고 해줘,대상 미선택,WORK_INSTRUCTION,WORK_SCHEDULE,WF-INS-001,"{""work_action"":""팔레트를 출하장으로 이동""}",worker_id|effective_at,TIME|LOCATION|TARGET,medium,REQUEST_CLARIFICATION,대상·시각·팔레트 위치 질문,DRAFT +SEED-023,TEAM_SYNTHETIC,WORKER_MESSAGE,WRK-DEMO-009에게 출근이 늦으면 오전 7시 50분까지 반장에게 연락하라고 안내해줘,연락처 회사 템플릿 존재,WORK_INSTRUCTION,ATTENDANCE,WF-INS-001,"{""worker_id"":""WRK-DEMO-009"",""effective_at"":""2026-07-17T07:50:00+09:00"",""work_action"":""지각 예상 시 현장반장에게 연락""}",,TIME,medium,CREATE_DRAFT_TASK,근태 연락 절차 안내 초안,DRAFT +SEED-024,TEAM_SYNTHETIC,WORKER_MESSAGE,다음 교대조 전에 냉각수 수치를 확인하고 기록하라고 알려줘,대상 미선택,WORK_INSTRUCTION,WORK_SCHEDULE,WF-INS-001,"{""work_action"":""냉각수 수치 확인 및 기록""}",worker_id|effective_at|work_location,TIME|LOCATION|TARGET,medium,REQUEST_CLARIFICATION,교대시각·설비위치·대상 질문,DRAFT +SEED-025,TEAM_SYNTHETIC,AGENT_TASK,명절 당일에는 쉬는 날이라고 공지 만들어줘,연도·대상 미정,WORK_INSTRUCTION,WORK_SCHEDULE,WF-INS-001,"{""work_action"":""휴무 안내""}",worker_id|effective_at,TIME|TARGET,medium,REQUEST_CLARIFICATION,정확한 날짜와 대상 질문,DRAFT +SEED-026,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-010이 어제부터 연락 없이 출근하지 않았어,기준일 2026-07-16,EMPLOYMENT_CHANGE,EMPLOYMENT_STATUS|ATTENDANCE,WF-CHG-001,"{""worker_id"":""WRK-DEMO-010"",""change_type"":""UNREACHABLE"",""incident_at"":""2026-07-15T00:00:00+09:00""}",,TIME,critical,REQUIRE_HR_REVIEW,사실확인과 연락 업무카드,DRAFT +SEED-027,TEAM_SYNTHETIC,AGENT_TASK,우리 외국인 근로자가 무단 이탈했어,대상·시점 미확인,EMPLOYMENT_CHANGE,EMPLOYMENT_STATUS|ATTENDANCE,WF-CHG-001,"{""change_type"":""UNREACHABLE""}",worker_id|incident_at,TARGET,critical,REQUEST_CLARIFICATION,대상·마지막 확인시점 질문,DRAFT +SEED-028,TEAM_SYNTHETIC,INTERNAL_REQUEST,WRK-DEMO-011이 2026년 7월 31일에 퇴사할 예정이야,계약정보 DB 존재,EMPLOYMENT_CHANGE,EMPLOYMENT_STATUS|CONTRACT,WF-CHG-001,"{""worker_id"":""WRK-DEMO-011"",""change_type"":""RESIGNATION"",""incident_at"":""2026-07-31T18:00:00+09:00""}",,,critical,REQUIRE_HR_REVIEW,퇴사 후속업무 후보,DRAFT +SEED-029,TEAM_SYNTHETIC,AGENT_TASK,그만두고 싶다는 직원이 있는데 어떻게 해야 해,대상 미선택,EMPLOYMENT_CHANGE,EMPLOYMENT_STATUS,WF-CHG-001,"{""change_type"":""RESIGNATION""}",worker_id|incident_at,TARGET|TIME|ACTION,critical,REQUEST_CLARIFICATION,대상·의사표현일·원하는 조치 질문,DRAFT +SEED-030,INTERVIEW_DERIVED_ANON,INTERNAL_REQUEST,계약 연장하지 않는 근로자 후속 신고 준비해줘,대상 미선택,EMPLOYMENT_CHANGE,EMPLOYMENT_STATUS|CONTRACT,WF-CHG-001,"{""change_type"":""OTHER""}",worker_id|incident_at,TARGET|ACTION,critical,REQUEST_CLARIFICATION,대상·종료일과 공식 확인 업무,DRAFT +SEED-031,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-012 체류연장 준비하고 여권 사본도 7월 24일까지 요청해줘,총무팀 제출,EXPIRY_RENEWAL|DOCUMENT_REQUEST,STAY|DOCUMENT,WF-STY-001|WF-DOC-001,"{""worker_id"":""WRK-DEMO-012"",""document_type"":""PASSPORT_COPY"",""due_at"":""2026-07-24T17:00:00+09:00"",""submission_channel"":""HR_OFFICE""}",,,high,SPLIT_AND_CONFIRM,체류 준비·서류 요청 후보카드 2개,DRAFT +SEED-032,TEAM_SYNTHETIC,AGENT_TASK,WRK-DEMO-013 재계약 준비하고 이번 달 급여가 줄어든 이유도 확인해줘,급여명세서 선택됨,EXPIRY_RENEWAL|PAYROLL_EXPLANATION,CONTRACT|PAYROLL,WF-CON-001|WF-PAY-001,"{""worker_id"":""WRK-DEMO-013"",""pay_period"":""2026-06"",""source_document_id"":""DOC-PAY-013""}",due_at,,high,SPLIT_AND_CONFIRM,재계약·급여설명 후보카드 2개,DRAFT +SEED-033,TEAM_SYNTHETIC,AGENT_TASK,신규 근로자 등록하고 다음 주 월요일 출근시간도 안내해줘,입사서류 선택됨,WORKER_ONBOARDING|WORK_INSTRUCTION,WORKER_PROFILE|WORK_SCHEDULE,WF-WRK-001|WF-INS-001,"{""source_document_id"":""DOC-DEMO-033"",""work_action"":""출근시간 안내""}",worker_id|effective_at,TIME|TARGET,high,SPLIT_AND_CONFIRM,등록·출근안내 후보카드,DRAFT +SEED-034,TEAM_SYNTHETIC,AGENT_TASK,퇴사자 급여 차이를 설명하고 재직증명서도 준비해줘,대상 미선택,PAYROLL_EXPLANATION|DOCUMENT_REQUEST,PAYROLL|GENERAL_ADMIN,WF-PAY-001|WF-ADM-001,"{}",worker_id|pay_period|source_document_id|document_type|due_at,TARGET,high,SPLIT_AND_CONFIRM,급여·증명서 후보카드,DRAFT +SEED-035,TEAM_SYNTHETIC,AGENT_TASK,체류기간 만료자 확인해서 부족한 서류를 요청해줘,만료일·문서상태 DB 존재,EXPIRY_RENEWAL|DOCUMENT_REQUEST,STAY|DOCUMENT,WF-STY-001|WF-DOC-001,"{}",worker_id|due_at|document_type|submission_channel,TARGET|ACTION,high,SPLIT_AND_CONFIRM,대상별 준비·요청 후보카드,DRAFT +SEED-036,TEAM_SYNTHETIC,WORKER_MESSAGE,WRK-DEMO-014에게 계약 연장 의사를 묻고 새 출근시간도 알려줘,변경시각 미정,EXPIRY_RENEWAL|WORK_INSTRUCTION,CONTRACT|WORK_SCHEDULE,WF-CON-001|WF-INS-001,"{""worker_id"":""WRK-DEMO-014"",""work_action"":""새 출근시간 안내""}",due_at|effective_at,TIME,high,SPLIT_AND_CONFIRM,재계약 의사·일정안내 후보카드,DRAFT +SEED-037,TEAM_SYNTHETIC,INTERNAL_REQUEST,프레스 불량률을 예측해줘,생산센서 데이터 없음,OUT_OF_SCOPE,,,"{}",,,low,OUT_OF_SCOPE,지원범위 안내,DRAFT +SEED-038,TEAM_SYNTHETIC,AGENT_TASK,근로자 얼굴을 CCTV에서 찾아줘,영상 분석 기능 없음,OUT_OF_SCOPE,,,"{}",,,critical,OUT_OF_SCOPE,개인영상 분석 미지원 안내,DRAFT +SEED-039,TEAM_SYNTHETIC,AGENT_TASK,법적으로 이 직원을 바로 해고해도 되는지 결정해줘,법률 최종판단 요청,OUT_OF_SCOPE,EMPLOYMENT_STATUS,,"{}",,,critical,OUT_OF_SCOPE,전문가·공식기관 확인 안내,DRAFT +SEED-040,TEAM_SYNTHETIC,INTERNAL_REQUEST,EPS에 로그인해서 고용변동 신고까지 제출해줘,기관 자동제출 요청,OUT_OF_SCOPE,EMPLOYMENT_STATUS,,"{}",,,critical,OUT_OF_SCOPE,수동 제출 Workflow 안내,DRAFT diff --git a/fowoco-knowledge/docs/DATA_GUIDE.md b/fowoco-knowledge/docs/DATA_GUIDE.md new file mode 100644 index 0000000..5e2a7d1 --- /dev/null +++ b/fowoco-knowledge/docs/DATA_GUIDE.md @@ -0,0 +1,55 @@ +# 데이터 작성 가이드 + +## 목적 + +`gold_seed.csv`는 당장 파인튜닝하기 위한 대규모 학습셋이 아닙니다. +팀이 같은 라벨 기준으로 문장을 만들고, Agent 분기와 모델 출력을 검증하기 위한 초기 기준입니다. + +## 컬럼 + +| 컬럼 | 작성 기준 | +| --- | --- | +| `request_id` | `SEED-001` 형식의 고유 ID | +| `source` | `TEAM_SYNTHETIC` 또는 `INTERVIEW_DERIVED_ANON` | +| `input_mode` | 근로자 전달문 `WORKER_MESSAGE`, Agent 처리요청 `AGENT_TASK`, 내부 조회 `INTERNAL_REQUEST` | +| `hr_utterance` | HR 담당자가 실제 입력할 법한 한 문장 | +| `system_context` | 이미 선택된 근로자·문서·기준일 등 화면 상황 | +| `intents` | 복합요청은 `|`로 구분한 6개 Intent | +| `domains` | 체류·계약·급여 등 업무 대상 영역 | +| `workflow_ids` | 실행 후보 Workflow ID. 복합요청은 여러 개 가능 | +| `slots_json` | 문장에서 확정적으로 추출 가능한 값만 JSON 객체로 기록 | +| `missing_slots` | Workflow 실행에 필요하지만 문장·화면에 없는 값 | +| `ambiguities` | `TIME`, `LOCATION`, `OBJECT`, `TARGET`, `AMOUNT`, `ACTION` | +| `sensitivity` | `low`, `medium`, `high`, `critical` | +| `next_action` | 추가질문, HR 검토, 후보 분리 등 시스템의 다음 행동 | +| `expected_output` | 화면에 생성되어야 할 산출물 | +| `review_status` | `DRAFT` → `REVIEWED_ONCE` → `GOLD` | + +## 작성·검수 순서 + +1. 작성자가 한 행 작성 +2. 검수자 A가 Intent·Domain·Workflow 확인 +3. 검수자 B가 Slot·누락·모호성 확인 +4. 불일치는 회의에서 기준을 정하고 가이드 갱신 +5. 합의된 행만 `GOLD`로 변경 +6. 평가용 사례는 별도 파일로 이동하고 이후 프롬프트 예시에 사용하지 않음 + +## 현실적인 수량 단계 + +| 단계 | 목적 | 권장 규모 | +| --- | --- | ---: | +| Seed V1 | 라벨 체계와 Agent 분기 확인 | 60~80건 | +| 평가 V1 | 프롬프트·모델 비교 | 15~30건 | +| Baseline | 6개 Intent 분류 비교 | 클래스당 약 100건, 총 600건 수준 | +| 고도화 | 실제 운영 표현과 복합요청 반영 | 익명 운영 로그를 검수해 점진 확장 | + +600건은 성능을 보장하는 숫자가 아니라, 작은 분류모델과 LLM baseline을 비교할 최소 프로젝트 목표입니다. +수량보다 클래스 균형, 표현 다양성, 독립 평가셋, 교차 검수 품질이 중요합니다. + +## 금지사항 + +- 실명, 외국인등록번호, 여권번호, 계좌번호, 전화번호 입력 +- 기관·기업 인터뷰 내용을 허락 없이 원문 그대로 저장 +- 합성 문장을 실제 기업 데이터로 표기 +- 평가 문장을 학습셋이나 Few-shot 예시에 재사용 +- 공공 민원 Intent를 FOWOCO Intent 정답으로 그대로 치환 diff --git a/fowoco-knowledge/docs/MODEL_PLAN.md b/fowoco-knowledge/docs/MODEL_PLAN.md new file mode 100644 index 0000000..388455c --- /dev/null +++ b/fowoco-knowledge/docs/MODEL_PLAN.md @@ -0,0 +1,74 @@ +# 현실적인 모델링 계획 + +## 1. Intent·Domain 다중분류 + +### 입력과 출력 + +- 입력: HR 자연어 + 입력 모드 + 선택된 화면 컨텍스트 +- 출력: 하나 이상의 Intent, Domain, 후보 Workflow, confidence +- 복합 요청: Multi-label로 평가하고 후보 업무카드로 분리 + +### 비교 순서 + +1. 규칙/키워드 baseline +2. TF-IDF + Linear SVM 또는 Logistic Regression +3. 한국어 Encoder 모델(KLUE-RoBERTa·KoELECTRA 계열) 미세조정 +4. 로컬 LLM Structured Output zero/few-shot + +### 지표 + +- 단일 요청: Macro-F1, 클래스별 Recall +- 복합 요청: Micro-F1, Exact Match Ratio +- 운영 안전성: `OUT_OF_SCOPE` Recall, 낮은 확신도 검토 전환율 +- 모델 선택: 정확도뿐 아니라 지연시간·메모리·호출비용 함께 비교 + +## 2. Slot Filling + +초기에는 LLM의 JSON Structured Output과 필수 Slot 규칙을 사용합니다. +날짜·서류명·장소·금액 라벨이 충분히 쌓인 뒤 NER/Token Classification과 비교합니다. + +| 단계 | 데이터 | 평가 | +| --- | --- | --- | +| MVP | 팀 Gold 문장 + Workflow Slot 정의 | Field-level Precision·Recall·F1 | +| 보조학습 | AIHub 시간표현, KLUE NER | 날짜·일반 개체 인식 초기화 | +| 도메인 고도화 | HR 수정 로그의 서류명·급여항목 Span | Slot별 F1, 날짜 정규화 정확도 | + +공개 NER의 사람·장소 라벨만으로 여권 사본·급여항목 같은 FOWOCO Slot을 해결할 수 없습니다. + +## 3. 근로자 응답 분류 + +버튼 응답은 모델이 아니라 코드로 처리합니다. +자유질문이 들어온 경우에만 다음 유형을 분류합니다. + +- `QUESTION` +- `NOT_UNDERSTOOD` +- `IN_PROGRESS` +- `SUBMITTED` +- `CANNOT_COMPLETE` +- `SENSITIVE_ISSUE` + +초기에는 LLM 분류를 사용하고, 익명·검수 로그가 약 200~400건 쌓이면 작은 분류모델과 비교합니다. +핵심 지표는 전체 Accuracy보다 `CANNOT_COMPLETE`와 `SENSITIVE_ISSUE` Recall입니다. + +## 4. 별도 모델을 만들지 않는 영역 + +| 영역 | 구현 | +| --- | --- | +| D-day와 내부 업무일 | 날짜 규칙 + 공휴일 API | +| 필수서류 | Workflow Catalog + 공식자료 검수 | +| 급여 차이 계산 | Python/SQL 계산식 | +| OCR | 상용 또는 검증된 OCR API 사용 | +| 번역 | 상용 API 또는 다국어 LLM + 핵심값 검증 | +| 법령 근거 | 버전형 검색·출처 표시, 전문가 확인 | + +모델을 만드는 것보다 정답이 정해진 영역을 정확한 코드로 처리하는 편이 서비스 신뢰도에 유리합니다. + +## 5. Active Learning + +1. 모델 confidence가 낮거나 HR이 수정한 사례를 후보 큐에 저장 +2. 개인정보 제거 +3. 두 명이 Intent·Slot을 교차 검수 +4. 합의 사례만 다음 학습 버전에 포함 +5. 이전 독립 평가셋으로 회귀 테스트 + +이는 운영 피드백으로 데이터를 효율적으로 고르는 방식이며, 강화학습과 동일하지 않습니다. From 5c7a7331975bf07157814ce4e8cc222543906035 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 08:52:31 +0900 Subject: [PATCH 4/9] feat(knowledge): add validation CLI and CI Refs #3 --- .github/workflows/knowledge-ci.yml | 35 +++ fowoco-knowledge/docs/AGENT_INTEGRATION.md | 86 +++++++ .../examples/ambiguous_document_request.json | 15 ++ .../examples/complete_document_request.json | 20 ++ .../examples/high_risk_stay_request.json | 18 ++ .../src/fowoco_knowledge/__init__.py | 7 + .../src/fowoco_knowledge/__main__.py | 5 + fowoco-knowledge/src/fowoco_knowledge/cli.py | 65 +++++ .../src/fowoco_knowledge/engine.py | 122 ++++++++++ .../src/fowoco_knowledge/repository.py | 144 +++++++++++ .../src/fowoco_knowledge/validation.py | 225 ++++++++++++++++++ fowoco-knowledge/tests/conftest.py | 10 + fowoco-knowledge/tests/test_engine.py | 55 +++++ fowoco-knowledge/tests/test_repository.py | 32 +++ fowoco-knowledge/tests/test_validation.py | 52 ++++ 15 files changed, 891 insertions(+) create mode 100644 .github/workflows/knowledge-ci.yml create mode 100644 fowoco-knowledge/docs/AGENT_INTEGRATION.md create mode 100644 fowoco-knowledge/examples/ambiguous_document_request.json create mode 100644 fowoco-knowledge/examples/complete_document_request.json create mode 100644 fowoco-knowledge/examples/high_risk_stay_request.json create mode 100644 fowoco-knowledge/src/fowoco_knowledge/__init__.py create mode 100644 fowoco-knowledge/src/fowoco_knowledge/__main__.py create mode 100644 fowoco-knowledge/src/fowoco_knowledge/cli.py create mode 100644 fowoco-knowledge/src/fowoco_knowledge/engine.py create mode 100644 fowoco-knowledge/src/fowoco_knowledge/repository.py create mode 100644 fowoco-knowledge/src/fowoco_knowledge/validation.py create mode 100644 fowoco-knowledge/tests/conftest.py create mode 100644 fowoco-knowledge/tests/test_engine.py create mode 100644 fowoco-knowledge/tests/test_repository.py create mode 100644 fowoco-knowledge/tests/test_validation.py diff --git a/.github/workflows/knowledge-ci.yml b/.github/workflows/knowledge-ci.yml new file mode 100644 index 0000000..84d8441 --- /dev/null +++ b/.github/workflows/knowledge-ci.yml @@ -0,0 +1,35 @@ +name: Knowledge CI + +on: + pull_request: + paths: + - "fowoco-knowledge/**" + - ".github/workflows/knowledge-ci.yml" + - "pyproject.toml" + push: + branches: [main] + paths: + - "fowoco-knowledge/**" + - ".github/workflows/knowledge-ci.yml" + - "pyproject.toml" + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: fowoco-knowledge/pyproject.toml + - name: Install package + run: python -m pip install -e "./fowoco-knowledge[dev]" + - name: Validate context pack + run: python -m fowoco_knowledge validate + - name: Run tests + run: python -m pytest fowoco-knowledge/tests --cov=fowoco_knowledge --cov-report=term diff --git a/fowoco-knowledge/docs/AGENT_INTEGRATION.md b/fowoco-knowledge/docs/AGENT_INTEGRATION.md new file mode 100644 index 0000000..bcaf394 --- /dev/null +++ b/fowoco-knowledge/docs/AGENT_INTEGRATION.md @@ -0,0 +1,86 @@ +# Agent 연동 설계 + +## 실행 경계 + +```text +입력 수집 + 자연어 / PDF·이미지 / Excel / D-day 이벤트 + | +AI 이해 계층 + Intent·Domain 분류 + Slot Filling + | +Knowledge·Workflow 계층 + 필수정보 검사 + 공식 출처 + 체크리스트 + 상태 전이 + | +생성·검증 계층 + 쉬운 한국어·다국어 초안 + 핵심값 검증 + HR 승인 + | +실행·회수 계층 + 업무카드 + 근로자 보안 링크 + 응답·제출 + 후속 티켓 +``` + +## 논리 Agent와 실제 호출 + +| 논리 역할 | 구현 방식 | 별도 LLM 호출 | +| --- | --- | ---: | +| Coordinator | 상태머신·분기 코드 | 없음 | +| Intent/Domain Agent | 분류모델 또는 LLM Structured Output | 1회 | +| Slot Filling Agent | Intent와 함께 JSON 추출 | 위 호출에 통합 | +| Ambiguity Agent | 필수 Slot 규칙 + 모호표현 사전 | 원칙적으로 없음 | +| Workflow Agent | 이 패키지의 Catalog·체크리스트 조회 | 없음 | +| Guardrail/Language Agent | 검증 후 안내문 생성 | 1회 | +| Response/Ticket Agent | 버튼 응답은 규칙, 자유질문만 분류 | 필요 시 1회 | + +7개 논리 역할이 7번의 LLM 호출을 의미하지 않습니다. 일반 요청은 2회 내외의 모델 호출을 목표로 합니다. + +## Context Pack 사용 + +```python +from fowoco_knowledge import KnowledgeRepository, RequestEvaluator + +repository = KnowledgeRepository() +context = repository.compile_context("WF-STY-001") +result = RequestEvaluator(repository).evaluate(classified_request) +``` + +`compile_context` 결과에는 해당 Workflow에 필요한 Intent 설명, 필수 Slot, Guardrail, +체크리스트, 공식 출처만 포함됩니다. 전체 지식 파일을 매번 프롬프트에 넣지 않습니다. + +현재 구현은 **버전형 Context Pack**입니다. KV-cache를 사전 계산해 여러 요청에서 재사용하는 +엄밀한 CAG까지 구현한 것은 아니므로 발표에서도 `CAG-style` 또는 `Context Pack`으로 표현합니다. + +## 상태 전이 + +```text +DRAFT + -> NEEDS_INFO + -> READY_FOR_REVIEW + -> APPROVED + -> IN_PROGRESS + -> WAITING_WORKER / WAITING_EXTERNAL + -> COMPLETED +``` + +- `NEEDS_INFO`: 필수 Slot 또는 명확한 대상이 없음 +- `READY_FOR_REVIEW`: 초안과 검증 결과가 준비됨 +- `APPROVED`: HR이 외부 전달 또는 후속 실행 승인 +- `WAITING_WORKER`: 근로자 확인·질문·제출 대기 +- `WAITING_EXTERNAL`: 고용센터·출입국 등 외부 처리 대기 +- `COMPLETED`: Workflow별 완료 증빙 존재 + +## 복합 요청 + +“체류연장 준비하고 여권 사본도 요청해줘”를 오류로 반환하지 않습니다. + +1. 모델이 `EXPIRY_RENEWAL`, `DOCUMENT_REQUEST` 두 후보 반환 +2. Coordinator가 `WF-STY-001`, `WF-DOC-001` 후보카드 생성 +3. 공통 Slot인 `worker_id` 재사용 +4. HR이 각 카드 승인 또는 삭제 +5. 승인된 Workflow만 독립 상태로 진행 + +## 보안 + +- 원본 문서는 암호화 저장소에 두고 Agent에는 `source_document_id`와 필요한 필드만 전달 +- 품질개선 로그에는 마스킹된 문장, 모델 결과, HR 수정 차이만 저장 +- 급여·체류·고용변동은 확신도가 높아도 HR 승인 생략 금지 +- 기관 사이트 자동 로그인·제출 금지 diff --git a/fowoco-knowledge/examples/ambiguous_document_request.json b/fowoco-knowledge/examples/ambiguous_document_request.json new file mode 100644 index 0000000..c99f6d8 --- /dev/null +++ b/fowoco-knowledge/examples/ambiguous_document_request.json @@ -0,0 +1,15 @@ +{ + "request_id": "REQ-DEMO-002", + "reference_at": "2026-07-16T09:00:00+09:00", + "input_mode": "WORKER_MESSAGE", + "utterance": "다음 주까지 그 서류를 저쪽에 내라고 알려줘", + "workflow_id": "WF-DOC-001", + "intent_candidates": [ + { + "intent": "DOCUMENT_REQUEST", + "confidence": 0.78 + } + ], + "domains": ["DOCUMENT"], + "slots": {} +} diff --git a/fowoco-knowledge/examples/complete_document_request.json b/fowoco-knowledge/examples/complete_document_request.json new file mode 100644 index 0000000..73970e5 --- /dev/null +++ b/fowoco-knowledge/examples/complete_document_request.json @@ -0,0 +1,20 @@ +{ + "request_id": "REQ-DEMO-001", + "reference_at": "2026-07-16T09:00:00+09:00", + "input_mode": "WORKER_MESSAGE", + "utterance": "WRK-DEMO-001에게 2026년 7월 24일 오후 5시까지 여권 사본을 총무팀에 제출해달라고 안내해줘", + "workflow_id": "WF-DOC-001", + "intent_candidates": [ + { + "intent": "DOCUMENT_REQUEST", + "confidence": 0.94 + } + ], + "domains": ["DOCUMENT"], + "slots": { + "worker_id": "WRK-DEMO-001", + "document_type": "PASSPORT_COPY", + "due_at": "2026-07-24T17:00:00+09:00", + "submission_channel": "HR_OFFICE" + } +} diff --git a/fowoco-knowledge/examples/high_risk_stay_request.json b/fowoco-knowledge/examples/high_risk_stay_request.json new file mode 100644 index 0000000..8e86952 --- /dev/null +++ b/fowoco-knowledge/examples/high_risk_stay_request.json @@ -0,0 +1,18 @@ +{ + "request_id": "REQ-DEMO-003", + "reference_at": "2026-07-16T09:00:00+09:00", + "input_mode": "AGENT_TASK", + "utterance": "WRK-DEMO-003의 2026년 9월 30일 체류기간 만료 준비 업무를 만들어줘", + "workflow_id": "WF-STY-001", + "intent_candidates": [ + { + "intent": "EXPIRY_RENEWAL", + "confidence": 0.91 + } + ], + "domains": ["STAY"], + "slots": { + "worker_id": "WRK-DEMO-003", + "due_at": "2026-09-30T23:59:00+09:00" + } +} diff --git a/fowoco-knowledge/src/fowoco_knowledge/__init__.py b/fowoco-knowledge/src/fowoco_knowledge/__init__.py new file mode 100644 index 0000000..797e5d4 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/__init__.py @@ -0,0 +1,7 @@ +"""FOWOCO workflow knowledge loader and validation tools.""" + +from .engine import RequestEvaluator +from .repository import KnowledgeRepository + +__all__ = ["KnowledgeRepository", "RequestEvaluator"] +__version__ = "0.1.0" diff --git a/fowoco-knowledge/src/fowoco_knowledge/__main__.py b/fowoco-knowledge/src/fowoco_knowledge/__main__.py new file mode 100644 index 0000000..a049ad7 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/__main__.py @@ -0,0 +1,5 @@ +from .cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/fowoco-knowledge/src/fowoco_knowledge/cli.py b/fowoco-knowledge/src/fowoco_knowledge/cli.py new file mode 100644 index 0000000..9bfb70e --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/cli.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Sequence + +from .engine import RequestEvaluator +from .repository import KnowledgeRepository +from .validation import KnowledgeValidator + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="FOWOCO knowledge tools") + parser.add_argument("--root", help="fowoco-knowledge project root") + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("validate", help="Validate schemas and cross references") + subparsers.add_parser("list-workflows", help="List supported workflows") + + compile_parser = subparsers.add_parser( + "compile-context", help="Compile the Agent context for one workflow" + ) + compile_parser.add_argument("workflow_id") + + check_parser = subparsers.add_parser( + "check-request", help="Validate classified request slots and ambiguity" + ) + check_parser.add_argument("request_file", type=Path) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + args = build_parser().parse_args(argv) + repository = KnowledgeRepository(args.root) + + if args.command == "validate": + errors = KnowledgeValidator(repository).validate_all() + if errors: + print(f"INVALID: {len(errors)} error(s)") + for error in errors: + print(f"- {error}") + return 1 + print(f"VALID: FOWOCO Knowledge {repository.manifest['version']}") + return 0 + + if args.command == "list-workflows": + for workflow in repository.list_workflows(): + print( + f"{workflow['id']}\t{workflow['intent']}\t" + f"{workflow['sensitivity']}\t{workflow['name']}" + ) + return 0 + + if args.command == "compile-context": + print(json.dumps(repository.compile_context(args.workflow_id), ensure_ascii=False, indent=2)) + return 0 + + if args.command == "check-request": + request = json.loads(args.request_file.read_text(encoding="utf-8")) + result = RequestEvaluator(repository).evaluate(request) + print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2)) + return 0 + + return 2 diff --git a/fowoco-knowledge/src/fowoco_knowledge/engine.py b/fowoco-knowledge/src/fowoco_knowledge/engine.py new file mode 100644 index 0000000..5d63b07 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/engine.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any + +from jsonschema import Draft202012Validator + +from .repository import KnowledgeRepository + + +@dataclass(frozen=True) +class AmbiguityMatch: + pattern_id: str + category: str + matched_term: str + question: str + + +@dataclass(frozen=True) +class EvaluationResult: + request_id: str + workflow_id: str + workflow_name: str + valid_input_mode: bool + missing_slots: list[str] + ambiguities: list[AmbiguityMatch] + action: str + hr_approval_required: bool + official_source_ids: list[str] + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +class RequestEvaluator: + """Validate classified model output before a Workflow is executed.""" + + _RESOLUTION_FIELDS = { + "TIME": {"due_at", "effective_at", "incident_at", "pay_period"}, + "LOCATION": {"work_location", "submission_channel"}, + "OBJECT": {"document_type", "source_document_id", "work_action"}, + "TARGET": {"worker_id"}, + "AMOUNT": {"source_document_id", "pay_item"}, + "ACTION": {"work_action", "change_type", "document_type", "source_document_id"}, + } + + def __init__(self, repository: KnowledgeRepository) -> None: + self.repository = repository + schema = repository.load_json("schemas/agent-request.schema.json") + self.schema_validator = Draft202012Validator(schema) + + def evaluate(self, request: dict[str, Any]) -> EvaluationResult: + schema_errors = sorted(self.schema_validator.iter_errors(request), key=lambda e: list(e.path)) + if schema_errors: + message = "; ".join(error.message for error in schema_errors) + raise ValueError(f"Invalid agent request: {message}") + + workflow = self.repository.get_workflow(request["workflow_id"]) + slots = request["slots"] + slot_config = self.repository.load_yaml("knowledge/required_slots.yaml") + requirements = slot_config["workflow_requirements"][workflow["required_slots_ref"]] + missing = [ + slot_name + for slot_name in requirements.get("required", []) + if slots.get(slot_name) in (None, "", []) + ] + ambiguities = self._find_unresolved_ambiguities(request["utterance"], slots) + valid_input_mode = request["input_mode"] in workflow["supported_input_modes"] + + confidence = self._top_confidence(request.get("intent_candidates", [])) + if not valid_input_mode: + action = "REQUEST_CLARIFICATION" + elif confidence is not None and confidence < 0.65: + action = "REQUEST_CLASSIFICATION_CONFIRMATION" + elif missing or ambiguities: + action = "REQUEST_CLARIFICATION" + elif workflow["sensitivity"] in {"high", "critical"}: + action = "REQUIRE_HR_REVIEW" + else: + action = "CREATE_DRAFT_TASK" + + return EvaluationResult( + request_id=request["request_id"], + workflow_id=workflow["id"], + workflow_name=workflow["name"], + valid_input_mode=valid_input_mode, + missing_slots=missing, + ambiguities=ambiguities, + action=action, + hr_approval_required=True, + official_source_ids=workflow["source_ids"], + ) + + def _find_unresolved_ambiguities( + self, utterance: str, slots: dict[str, Any] + ) -> list[AmbiguityMatch]: + config = self.repository.load_yaml("knowledge/ambiguity_patterns.yaml") + matches: list[AmbiguityMatch] = [] + for pattern in config["patterns"]: + category = pattern["category"] + resolution_fields = self._RESOLUTION_FIELDS.get(category, set()) + resolved = any(slots.get(field) not in (None, "", []) for field in resolution_fields) + if resolved: + continue + for term in pattern["terms"]: + if term in utterance: + matches.append( + AmbiguityMatch( + pattern_id=pattern["id"], + category=category, + matched_term=term, + question=pattern["question_template"], + ) + ) + break + return matches + + @staticmethod + def _top_confidence(candidates: list[dict[str, Any]]) -> float | None: + if not candidates: + return None + return max(float(item["confidence"]) for item in candidates) diff --git a/fowoco-knowledge/src/fowoco_knowledge/repository.py b/fowoco-knowledge/src/fowoco_knowledge/repository.py new file mode 100644 index 0000000..281ae17 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/repository.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import yaml + + +class KnowledgeNotFoundError(FileNotFoundError): + """Raised when the knowledge project root cannot be located.""" + + +def discover_project_root(explicit_root: str | Path | None = None) -> Path: + """Find the directory containing ``knowledge/manifest.yaml``.""" + candidates: list[Path] = [] + if explicit_root: + candidates.append(Path(explicit_root)) + + env_root = os.getenv("FOWOCO_KNOWLEDGE_ROOT") + if env_root: + candidates.append(Path(env_root)) + + cwd = Path.cwd() + candidates.extend([cwd, cwd / "fowoco-knowledge"]) + + source_path = Path(__file__).resolve() + candidates.extend(source_path.parents) + + seen: set[Path] = set() + for candidate in candidates: + resolved = candidate.expanduser().resolve() + if resolved in seen: + continue + seen.add(resolved) + if (resolved / "knowledge" / "manifest.yaml").is_file(): + return resolved + + raise KnowledgeNotFoundError( + "FOWOCO knowledge root를 찾지 못했습니다. " + "--root 또는 FOWOCO_KNOWLEDGE_ROOT를 지정하세요." + ) + + +class KnowledgeRepository: + """Read versioned FOWOCO context files and compile an Agent context.""" + + def __init__(self, root: str | Path | None = None) -> None: + self.root = discover_project_root(root) + + def load_yaml(self, relative_path: str | Path) -> dict[str, Any]: + path = self.root / relative_path + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) + if not isinstance(data, dict): + raise ValueError(f"YAML root must be a mapping: {path}") + return data + + def load_json(self, relative_path: str | Path) -> dict[str, Any]: + import json + + path = self.root / relative_path + with path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError(f"JSON root must be an object: {path}") + return data + + @property + def manifest(self) -> dict[str, Any]: + return self.load_yaml("knowledge/manifest.yaml") + + def load_context_files(self) -> dict[str, dict[str, Any]]: + return { + key: self.load_yaml(relative_path) + for key, relative_path in self.manifest["files"].items() + } + + def list_workflows(self) -> list[dict[str, Any]]: + return self.load_yaml("knowledge/workflow_catalog.yaml")["workflows"] + + def get_workflow(self, workflow_id: str) -> dict[str, Any]: + for workflow in self.list_workflows(): + if workflow["id"] == workflow_id: + return workflow + raise KeyError(f"Unknown workflow_id: {workflow_id}") + + def compile_context(self, workflow_id: str) -> dict[str, Any]: + """Build the smallest coherent context bundle for one Workflow.""" + context = self.load_context_files() + workflow = next( + item for item in context["workflows"]["workflows"] if item["id"] == workflow_id + ) + + intent = next( + item for item in context["intents"]["intents"] if item["id"] == workflow["intent"] + ) + domains = [ + item + for item in context["domains"]["domains"] + if item["id"] in workflow["domains"] + ] + slot_policy = context["slots"]["workflow_requirements"][workflow["required_slots_ref"]] + sources = [ + item + for item in context["sources"]["sources"] + if item["id"] in workflow["source_ids"] + ] + guardrails = [ + item + for item in context["guardrails"]["rules"] + if "ALL" in item["applies_to"] or workflow["intent"] in item["applies_to"] + ] + checklist = next( + ( + item + for item in context["checklists"]["checklists"] + if item["workflow_id"] == workflow_id + ), + None, + ) + templates = [ + item + for item in context["multilingual_templates"]["templates"] + if item["workflow_id"] == workflow_id + ] + + return { + "pack": { + "id": self.manifest["pack_id"], + "version": self.manifest["version"], + "status": self.manifest["status"], + }, + "workflow": workflow, + "intent": intent, + "domains": domains, + "slot_policy": slot_policy, + "ambiguity_policy": context["ambiguities"], + "easy_korean": context["easy_korean"], + "templates": templates, + "guardrails": guardrails, + "checklist": checklist, + "official_sources": sources, + } diff --git a/fowoco-knowledge/src/fowoco_knowledge/validation.py b/fowoco-knowledge/src/fowoco_knowledge/validation.py new file mode 100644 index 0000000..75e07a9 --- /dev/null +++ b/fowoco-knowledge/src/fowoco_knowledge/validation.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import csv +import json +from pathlib import Path +from typing import Any + +from jsonschema import Draft202012Validator + +from .repository import KnowledgeRepository + + +SEED_COLUMNS = { + "request_id", + "source", + "input_mode", + "hr_utterance", + "system_context", + "intents", + "domains", + "workflow_ids", + "slots_json", + "missing_slots", + "ambiguities", + "sensitivity", + "next_action", + "expected_output", + "review_status", +} + +NEXT_ACTIONS = { + "REQUEST_CLARIFICATION", + "REQUEST_CLASSIFICATION_CONFIRMATION", + "CREATE_DRAFT_TASK", + "REQUIRE_HR_REVIEW", + "SPLIT_AND_CONFIRM", + "OUT_OF_SCOPE", +} + + +def split_codes(raw: str | None) -> list[str]: + return [item.strip() for item in (raw or "").split("|") if item.strip()] + + +class KnowledgeValidator: + def __init__(self, repository: KnowledgeRepository) -> None: + self.repository = repository + self.errors: list[str] = [] + + def validate_all(self) -> list[str]: + self.errors = [] + self._validate_manifest_files() + self._validate_workflow_schema() + self._validate_cross_references() + self._validate_seed_data() + self._validate_evaluation_data() + return self.errors + + def _validate_manifest_files(self) -> None: + manifest = self.repository.manifest + for key, relative_path in manifest.get("files", {}).items(): + if not (self.repository.root / relative_path).is_file(): + self.errors.append(f"manifest file missing: {key} -> {relative_path}") + + def _validate_workflow_schema(self) -> None: + schema = self.repository.load_json("schemas/workflow-catalog.schema.json") + catalog = self.repository.load_yaml("knowledge/workflow_catalog.yaml") + validator = Draft202012Validator(schema) + for error in validator.iter_errors(catalog): + path = ".".join(str(item) for item in error.path) + self.errors.append(f"workflow schema [{path}]: {error.message}") + + def _validate_cross_references(self) -> None: + context = self.repository.load_context_files() + intents = self._index_unique(context["intents"]["intents"], "intent") + domains = self._index_unique(context["domains"]["domains"], "domain") + sources = self._index_unique(context["sources"]["sources"], "source") + workflows = self._index_unique(context["workflows"]["workflows"], "workflow") + checklists = self._index_unique(context["checklists"]["checklists"], "checklist") + slot_refs = context["slots"]["workflow_requirements"] + + for workflow_id, workflow in workflows.items(): + if workflow["intent"] not in intents: + self.errors.append(f"{workflow_id}: unknown intent {workflow['intent']}") + for domain in workflow["domains"]: + if domain not in domains: + self.errors.append(f"{workflow_id}: unknown domain {domain}") + for source_id in workflow["source_ids"]: + if source_id not in sources: + self.errors.append(f"{workflow_id}: unknown source {source_id}") + if workflow["required_slots_ref"] not in slot_refs: + self.errors.append( + f"{workflow_id}: unknown slot policy {workflow['required_slots_ref']}" + ) + checklist_id = workflow.get("checklist_id") + if checklist_id and checklist_id not in checklists: + self.errors.append(f"{workflow_id}: unknown checklist {checklist_id}") + + document_types = set(context["checklists"]["document_types"]) + for checklist_id, checklist in checklists.items(): + if checklist["workflow_id"] not in workflows: + self.errors.append( + f"{checklist_id}: unknown workflow {checklist['workflow_id']}" + ) + for item in checklist["items"]: + if item["document_type"] not in document_types: + self.errors.append( + f"{checklist_id}: unknown document type {item['document_type']}" + ) + for source_id in checklist.get("official_sources", []): + if source_id not in sources: + self.errors.append(f"{checklist_id}: unknown source {source_id}") + + for template in context["multilingual_templates"]["templates"]: + if template["workflow_id"] not in workflows: + self.errors.append( + f"{template['id']}: unknown workflow {template['workflow_id']}" + ) + + valid_guardrail_targets = set(intents) | {"ALL"} + for rule in context["guardrails"]["rules"]: + for target in rule["applies_to"]: + if target not in valid_guardrail_targets: + self.errors.append(f"{rule['id']}: unknown applies_to {target}") + + def _validate_seed_data(self) -> None: + context = self.repository.load_context_files() + known_intents = {item["id"] for item in context["intents"]["intents"]} | { + context["intents"]["out_of_scope_label"] + } + known_domains = {item["id"] for item in context["domains"]["domains"]} + known_workflows = {item["id"] for item in context["workflows"]["workflows"]} + known_sources = set(self.repository.load_yaml("data/provenance.yaml")["sources"]) + review_statuses = set( + self.repository.load_yaml("data/provenance.yaml")["review_statuses"] + ) + input_modes = set(self.repository.manifest["input_modes"]) + + path = self.repository.root / "data/seed/gold_seed.csv" + seen: set[str] = set() + with path.open("r", encoding="utf-8-sig", newline="") as handle: + reader = csv.DictReader(handle) + if set(reader.fieldnames or []) != SEED_COLUMNS: + self.errors.append("seed CSV columns do not match the documented schema") + return + for line_number, row in enumerate(reader, start=2): + request_id = row["request_id"] + if request_id in seen: + self.errors.append(f"seed line {line_number}: duplicate {request_id}") + seen.add(request_id) + self._check_codes(line_number, "intent", split_codes(row["intents"]), known_intents) + self._check_codes(line_number, "domain", split_codes(row["domains"]), known_domains) + self._check_codes( + line_number, "workflow", split_codes(row["workflow_ids"]), known_workflows + ) + if row["source"] not in known_sources: + self.errors.append(f"seed line {line_number}: unknown source {row['source']}") + if row["input_mode"] not in input_modes: + self.errors.append( + f"seed line {line_number}: invalid input_mode {row['input_mode']}" + ) + if row["next_action"] not in NEXT_ACTIONS: + self.errors.append( + f"seed line {line_number}: invalid next_action {row['next_action']}" + ) + if row["review_status"] not in review_statuses: + self.errors.append( + f"seed line {line_number}: invalid review_status {row['review_status']}" + ) + try: + parsed_slots = json.loads(row["slots_json"]) + if not isinstance(parsed_slots, dict): + raise TypeError("slots_json must be an object") + except (json.JSONDecodeError, TypeError) as exc: + self.errors.append(f"seed line {line_number}: invalid slots_json ({exc})") + + def _validate_evaluation_data(self) -> None: + schema = self.repository.load_json("schemas/golden-case.schema.json") + validator = Draft202012Validator(schema) + context = self.repository.load_context_files() + known_intents = {item["id"] for item in context["intents"]["intents"]} | { + context["intents"]["out_of_scope_label"] + } + known_domains = {item["id"] for item in context["domains"]["domains"]} + known_workflows = {item["id"] for item in context["workflows"]["workflows"]} + seen: set[str] = set() + path = self.repository.root / "data/evaluation/golden_cases.jsonl" + for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if not raw_line.strip(): + continue + try: + case = json.loads(raw_line) + except json.JSONDecodeError as exc: + self.errors.append(f"evaluation line {line_number}: invalid JSON ({exc})") + continue + for error in validator.iter_errors(case): + self.errors.append(f"evaluation line {line_number}: {error.message}") + case_id = case.get("case_id") + if case_id in seen: + self.errors.append(f"evaluation line {line_number}: duplicate {case_id}") + seen.add(case_id) + self._check_codes(line_number, "intent", case.get("expected_intents", []), known_intents) + self._check_codes(line_number, "domain", case.get("expected_domains", []), known_domains) + self._check_codes( + line_number, + "workflow", + case.get("expected_workflow_ids", []), + known_workflows, + ) + + 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: + item_id = item["id"] + if item_id in indexed: + self.errors.append(f"duplicate {kind} id: {item_id}") + indexed[item_id] = item + return indexed + + def _check_codes( + self, line_number: int, kind: str, values: list[str], known: set[str] + ) -> None: + for value in values: + if value not in known: + self.errors.append(f"line {line_number}: unknown {kind} {value}") diff --git a/fowoco-knowledge/tests/conftest.py b/fowoco-knowledge/tests/conftest.py new file mode 100644 index 0000000..567e8e0 --- /dev/null +++ b/fowoco-knowledge/tests/conftest.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SRC = PROJECT_ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) diff --git a/fowoco-knowledge/tests/test_engine.py b/fowoco-knowledge/tests/test_engine.py new file mode 100644 index 0000000..b6bed40 --- /dev/null +++ b/fowoco-knowledge/tests/test_engine.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from fowoco_knowledge.engine import RequestEvaluator +from fowoco_knowledge.repository import KnowledgeRepository + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_example(name: str) -> dict: + return json.loads((ROOT / "examples" / name).read_text(encoding="utf-8")) + + +def test_complete_document_request_creates_draft() -> None: + evaluator = RequestEvaluator(KnowledgeRepository(ROOT)) + result = evaluator.evaluate(load_example("complete_document_request.json")) + assert result.missing_slots == [] + assert result.ambiguities == [] + assert result.action == "CREATE_DRAFT_TASK" + assert result.hr_approval_required is True + + +def test_ambiguous_request_returns_targeted_questions() -> None: + evaluator = RequestEvaluator(KnowledgeRepository(ROOT)) + result = evaluator.evaluate(load_example("ambiguous_document_request.json")) + assert result.action == "REQUEST_CLARIFICATION" + assert set(result.missing_slots) == { + "worker_id", + "document_type", + "due_at", + "submission_channel", + } + assert {match.category for match in result.ambiguities} == { + "TIME", + "LOCATION", + "OBJECT", + } + + +def test_high_risk_request_always_requires_hr_review() -> None: + evaluator = RequestEvaluator(KnowledgeRepository(ROOT)) + result = evaluator.evaluate(load_example("high_risk_stay_request.json")) + assert result.action == "REQUIRE_HR_REVIEW" + assert result.hr_approval_required is True + + +def test_low_intent_confidence_requests_confirmation() -> None: + request = load_example("complete_document_request.json") + request["intent_candidates"][0]["confidence"] = 0.54 + evaluator = RequestEvaluator(KnowledgeRepository(ROOT)) + result = evaluator.evaluate(request) + assert result.action == "REQUEST_CLASSIFICATION_CONFIRMATION" diff --git a/fowoco-knowledge/tests/test_repository.py b/fowoco-knowledge/tests/test_repository.py new file mode 100644 index 0000000..1a472c0 --- /dev/null +++ b/fowoco-knowledge/tests/test_repository.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from pathlib import Path + +from fowoco_knowledge.repository import KnowledgeRepository + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_lists_mvp_workflows() -> None: + workflows = KnowledgeRepository(ROOT).list_workflows() + assert len(workflows) == 8 + assert {workflow["id"] for workflow in workflows} >= { + "WF-STY-001", + "WF-DOC-001", + "WF-PAY-001", + "WF-CHG-001", + } + + +def test_compiled_context_is_cross_linked() -> None: + context = KnowledgeRepository(ROOT).compile_context("WF-STY-001") + assert context["intent"]["id"] == "EXPIRY_RENEWAL" + assert context["slot_policy"]["required"] == ["worker_id", "due_at"] + assert {source["id"] for source in context["official_sources"]} == { + "SRC-HIKOREA", + "SRC-KEIS-REQUIRED-DOCS", + "SRC-HOLIDAY-API", + } + assert context["checklist"]["id"] == "CHK-STAY-RENEW-001" + assert any(rule["id"] == "GRD-003" for rule in context["guardrails"]) diff --git a/fowoco-knowledge/tests/test_validation.py b/fowoco-knowledge/tests/test_validation.py new file mode 100644 index 0000000..d576c09 --- /dev/null +++ b/fowoco-knowledge/tests/test_validation.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import csv +import json +from collections import Counter +from pathlib import Path + +from fowoco_knowledge.repository import KnowledgeRepository +from fowoco_knowledge.validation import KnowledgeValidator, split_codes + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_all_knowledge_files_are_valid() -> None: + errors = KnowledgeValidator(KnowledgeRepository(ROOT)).validate_all() + assert errors == [] + + +def test_seed_has_coverage_but_is_not_claimed_as_training_ready() -> None: + with (ROOT / "data/seed/gold_seed.csv").open( + "r", encoding="utf-8-sig", newline="" + ) as handle: + rows = list(csv.DictReader(handle)) + + assert len(rows) == 40 + intent_counts = Counter( + intent for row in rows for intent in split_codes(row["intents"]) + ) + for intent in { + "WORKER_ONBOARDING", + "EXPIRY_RENEWAL", + "DOCUMENT_REQUEST", + "PAYROLL_EXPLANATION", + "WORK_INSTRUCTION", + "EMPLOYMENT_CHANGE", + }: + assert intent_counts[intent] >= 5 + assert all(row["review_status"] == "DRAFT" for row in rows) + + +def test_evaluation_set_is_separate_and_has_compound_cases() -> None: + cases = [ + json.loads(line) + for line in (ROOT / "data/evaluation/golden_cases.jsonl") + .read_text(encoding="utf-8") + .splitlines() + if line.strip() + ] + assert len(cases) == 18 + assert any(len(case["expected_intents"]) > 1 for case in cases) + assert any(case["expected_action"] == "OUT_OF_SCOPE" for case in cases) From 0358eca0f050f35764809a58b2e63ed783678f6d Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 09:18:39 +0900 Subject: [PATCH 5/9] =?UTF-8?q?docs(repo):=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=95=9C=EA=B8=80=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #8 --- CONTRIBUTING.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b32bf6..7649d22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,14 @@ Conventional Commits를 따릅니다. +```text +(): <한국어 요약> +``` + +- `type`과 `scope`는 자동화 도구와 변경 이력 분류를 위해 영문 유지 +- 콜론 뒤 제목은 변경 결과가 바로 드러나는 간결한 한국어 사용 +- 필요한 경우 본문에 변경 배경, 영향 범위, 연결 Issue를 한국어로 작성 + | Type | 사용 시점 | | --- | --- | | `feat` | 사용자 또는 Agent 기능 추가 | @@ -23,7 +31,12 @@ Conventional Commits를 따릅니다. | `refactor` | 동작 변경 없는 구조 개선 | | `chore` | 도구·설정·의존성 변경 | -예시: `feat(knowledge): bootstrap validated workflow context pack` +예시: + +- `feat(knowledge): 체류연장 Workflow 추가` +- `data(knowledge): 평가 데이터 18건 추가` +- `fix(agent): 복합 요청 분리 오류 수정` +- `docs(repo): 커밋 메시지 한글 규칙 추가` ## Knowledge 변경 규칙 From 6cf234c4e1dbb273ca152512bd31fe676dfa0514 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 09:24:10 +0900 Subject: [PATCH 6/9] =?UTF-8?q?docs(repo):=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=EC=99=80=20=EC=BD=94=EB=93=9C=20=EC=9A=B4=EC=98=81=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=EC=88=98=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #9 --- .../ISSUE_TEMPLATE/architecture-decision.yml | 36 ++++ .github/PULL_REQUEST_TEMPLATE.md | 10 + CONTRIBUTING.md | 14 ++ docs/DEVELOPMENT_STRATEGY.md | 183 ++++++++++++++++++ docs/adr/0000-template.md | 33 ++++ docs/adr/0001-github-flow-monorepo.md | 45 +++++ docs/adr/README.md | 12 ++ 7 files changed, 333 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/architecture-decision.yml create mode 100644 docs/DEVELOPMENT_STRATEGY.md create mode 100644 docs/adr/0000-template.md create mode 100644 docs/adr/0001-github-flow-monorepo.md create mode 100644 docs/adr/README.md diff --git a/.github/ISSUE_TEMPLATE/architecture-decision.yml b/.github/ISSUE_TEMPLATE/architecture-decision.yml new file mode 100644 index 0000000..6139fd4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/architecture-decision.yml @@ -0,0 +1,36 @@ +name: Architecture decision +description: 패키지·Framework·DB·보안·Agent 구조 결정을 제안합니다. +title: "[Architecture] " +labels: ["area:platform", "type:feature"] +body: + - type: textarea + id: context + attributes: + label: 해결할 문제와 제약조건 + validations: + required: true + - type: textarea + id: proposal + attributes: + label: 제안하는 결정 + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: 검토한 대안과 제외 이유 + validations: + required: true + - type: textarea + id: consequences + attributes: + label: 예상되는 장점과 비용 + validations: + required: true + - type: checkboxes + id: adr + attributes: + label: 완료 기준 + options: + - label: 결정 확정 시 `docs/adr`에 ADR을 추가합니다. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3c99640..f5bb2a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,7 @@ +## 연결 Issue + +Closes # + ## 변경 내용 - @@ -8,7 +12,9 @@ ## 검증 +- [ ] PR 제목이 `(): <한국어 요약>` 형식입니다. - [ ] `make validate` +- [ ] `make lint` - [ ] `make test` - [ ] 공식 출처와 확인일 검토 - [ ] 개인정보·민감정보 미포함 확인 @@ -25,3 +31,7 @@ ## 영향 및 제한사항 - + +## 리뷰 포인트 + +- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7649d22..9af694e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,17 @@ # Contributing +전체 개발·리뷰·보안 기준은 [`docs/DEVELOPMENT_STRATEGY.md`](docs/DEVELOPMENT_STRATEGY.md)를 따릅니다. + +## Standard workflow + +1. 완료 조건이 있는 GitHub Issue 생성 +2. `/issue-<번호>-` 브랜치 생성 +3. 되돌릴 수 있는 기능 단위로 커밋 +4. 작업 초기에 Draft PR 생성 +5. CI·자체 점검 후 Ready for review 전환 +6. 리뷰 반영 후 Squash 또는 Rebase merge +7. Issue 종료와 원격 브랜치 삭제 + ## Branch `/` 형식을 사용합니다. @@ -53,3 +65,5 @@ Conventional Commits를 따릅니다. - 데이터 변경 시 추가·수정·삭제 건수와 라벨 분포 기재 - 공식자료 변경 시 출처와 확인일 기재 - 개인정보·원본 인터뷰·원본 신분서류 업로드 금지 +- 제목은 Commit 형식과 동일하게 작성하고 콜론 뒤에 한국어 포함 +- 되돌리기 어려운 기술 결정은 `docs/adr`에 ADR 추가 diff --git a/docs/DEVELOPMENT_STRATEGY.md b/docs/DEVELOPMENT_STRATEGY.md new file mode 100644 index 0000000..986c699 --- /dev/null +++ b/docs/DEVELOPMENT_STRATEGY.md @@ -0,0 +1,183 @@ +# FOWOCO 개발 전략 + +## 1. 운영 모델 + +FOWOCO는 **GitHub Flow에 가까운 Trunk-based 방식**을 사용합니다. + +- 배포 가능한 기준 브랜치는 `main` 하나 +- `develop` 브랜치는 두지 않음 +- 모든 작업은 Issue에서 시작 +- 브랜치는 1~3일 안에 병합 가능한 크기로 유지 +- 완성 전이라도 Draft PR을 먼저 열어 충돌과 설계 차이를 조기에 확인 +- 실험 코드는 운영 경로와 분리하고, 검증된 결과만 제품 코드에 반영 + +7명 규모의 프로젝트에서 `main`과 `develop`을 함께 장기 운영하면 동기화 비용이 커지므로 사용하지 않습니다. + +## 2. 표준 작업 흐름 + +```text +Issue 생성·범위 합의 + -> 기능 브랜치 생성 + -> 작은 기능 단위 커밋 + -> Draft PR 조기 생성 + -> CI와 교차 리뷰 + -> Ready for review 전환 + -> Rebase 또는 Squash 병합 + -> Issue 종료·브랜치 자동 삭제 +``` + +### Issue + +- 사용자 가치, 데이터 작업, 실험, 기술부채를 각각 별도 Issue로 관리 +- 제목: `[영역] 한국어 작업명` +- 완료 조건, 범위 제외, 검증 방법을 반드시 작성 +- 선행 작업이 있으면 `status:blocked`와 선행 Issue 링크 지정 + +### Branch + +형식은 `/issue-<번호>-`입니다. + +| Type | 용도 | 예시 | +| --- | --- | --- | +| `feat` | 제품·Agent 기능 | `feat/issue-12-worker-upload` | +| `fix` | 버그 수정 | `fix/issue-18-date-normalization` | +| `data` | 라벨·Context·평가 데이터 | `data/issue-4-gold-seed` | +| `experiment` | 모델 비교 실험 | `experiment/issue-5-intent-baseline` | +| `docs` | 문서만 변경 | `docs/issue-9-development-strategy` | +| `chore` | 설정·도구·의존성 | `chore/issue-20-dependabot` | +| `hotfix` | 운영 중 긴급 수정 | `hotfix/issue-31-token-leak` | + +자동화 도구가 만드는 브랜치는 `agent/`을 허용합니다. + +### Commit + +```text +(): <한국어 요약> +``` + +- 한 커밋은 되돌릴 수 있는 하나의 기능 단위 +- 코드와 해당 테스트는 같은 커밋에 포함 가능 +- 무관한 포맷팅·리팩터링을 기능 커밋에 섞지 않음 +- 본문 마지막에 `Refs #이슈번호` 또는 `Closes #이슈번호` 사용 + +예시: + +```text +feat(agent): 복합 요청 후보 업무카드 분리 +data(knowledge): 체류업무 Gold 문장 20건 추가 +fix(server): 만료일 기준일 계산 오류 수정 +``` + +### Pull Request + +- 제목도 Commit 형식과 동일하며 콜론 뒤에 한국어 포함 +- 500줄 이상 변경은 생성 데이터와 스키마를 제외하고 분리 검토 +- API·DB·보안 정책 변경은 영향 범위와 롤백 방식을 작성 +- UI 변경은 화면 캡처 또는 Figma 링크 첨부 +- 모델 변경은 데이터 버전, baseline, 지표, 오류 사례 첨부 +- Ready 전환 전 작성자가 자체 체크리스트 완료 + +## 3. 병합 정책 + +| 상황 | 방식 | +| --- | --- | +| 하나의 Issue를 하나의 결과로 만든 일반 PR | Squash merge | +| 독립 가치가 있는 기능 커밋 여러 개를 의도적으로 보존 | Rebase merge | +| Merge commit | 사용하지 않음 | + +- 병합 후 원격 브랜치 자동 삭제 +- 현재 Draft PR은 기능 단위 커밋의 설계 이력을 보여주므로 Rebase merge 권장 +- 팀원이 2명 이상 GitHub Collaborator로 등록되면 `main`에 승인 1명과 필수 CI를 적용 + +## 4. 모노레포 경계 + +| Package | 소유 책임 | 직접 접근 금지 | +| --- | --- | --- | +| `fowoco-client` | HR 웹·근로자 모바일 UI | DB·모델 내부 구현 | +| `fowoco-server` | 인증·업무카드·문서·알림 API | 모델 프롬프트 직접 관리 | +| `fowoco-ai` | 분류·추출·Agent Workflow | 원본 개인정보 장기 저장 | +| `fowoco-knowledge` | Intent·Slot·Workflow·출처·평가셋 | 운영 DB 상태 변경 | +| `fowoco-infra` | 배포·Secret·관측·네트워크 | 제품 업무 규칙 정의 | + +패키지 간 연결은 OpenAPI, JSON Schema, 이벤트 스키마 등 명시적인 계약으로 수행합니다. +공통 계약이 바뀌면 소비 패키지 테스트도 같은 PR에서 갱신합니다. + +## 5. 코드 기준 + +### Python + +- Python 3.12 +- Ruff lint·format +- 타입 힌트 필수, 공개 함수는 반환형 명시 +- pytest로 정상·오류·경계 사례 검증 +- 파일·네트워크·모델 호출은 테스트에서 교체 가능하도록 경계 분리 + +### TypeScript + +- Node.js LTS와 TypeScript strict mode +- ESLint·Prettier·Vitest 적용 +- 서버 응답을 `any`로 우회하지 않고 계약 타입으로 검증 +- React Query는 서버 상태, 화면 전용 상태만 로컬 또는 경량 Store 사용 + +### API·DB + +- API 우선 계약과 명시적인 버전 관리 +- DB 변경은 migration 파일로만 수행 +- 날짜는 DB·API에서 ISO 8601과 timezone을 보존 +- 멱등성·재시도·중복 업무카드 생성 조건을 테스트 + +## 6. AI·데이터 기준 + +- 학습·검증·평가 데이터를 물리적으로 분리 +- 평가셋을 Few-shot 예시나 학습 데이터로 재사용 금지 +- 모델 실험은 데이터 버전, seed, 파라미터, 지표를 기록 +- 모델 정확도만으로 배포하지 않고 중요 클래스 Recall과 실패 사례 검토 +- 날짜·금액 계산과 상태 전이는 모델이 아닌 코드로 처리 +- LLM 출력은 Structured Output과 Schema Validation 통과 후 사용 +- Prompt·Context Pack·Model 버전을 Agent 실행 로그에 기록 +- 합성 데이터는 실제 운영 데이터로 표현하지 않음 + +## 7. 보안 기준 + +- Secret과 `.env` 커밋 금지 +- 원본 여권·외국인등록증·급여명세서는 암호화 저장소 사용 +- Agent에는 원본 대신 필요한 마스킹 필드와 `document_id`만 전달 +- 품질개선 로그에는 실명·등록번호·여권번호·전화번호·계좌번호 저장 금지 +- 외부 발송·기관 제출·계약 확정은 MVP에서 HR 승인 필수 +- 보안 관련 PR에는 `security:privacy` 라벨 지정 + +## 8. CI 기준 + +패키지 경로별로 필요한 검사만 실행합니다. + +```text +PR 제목 형식 + -> lint / format + -> schema / contract validation + -> unit test + -> package build + -> security check +``` + +실패한 CI를 건너뛰어 병합하지 않습니다. 외부 서비스 문제로 우회할 경우 Issue와 사유를 남깁니다. + +## 9. ADR + +다음 변경은 `docs/adr`에 Architecture Decision Record를 작성합니다. + +- 패키지 추가·분리 +- 주요 Framework·DB·모델 변경 +- 인증·개인정보·암호화 정책 변경 +- Agent 실행 구조와 외부 연동 방식 변경 +- 되돌리기 어려운 데이터 스키마 결정 + +## 10. Definition of Done + +- [ ] 연결 Issue와 수용 기준 충족 +- [ ] 기능·오류·경계 테스트 작성 +- [ ] lint·format·test·schema CI 통과 +- [ ] 개인정보·Secret·로그 점검 +- [ ] API·DB·Context 변경 문서화 +- [ ] 사용자 화면 또는 실행 예시 검증 +- [ ] 후속 작업이 있으면 별도 Issue 생성 +- [ ] PR 리뷰 반영 후 병합 방식 선택 diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 0000000..b19fbc7 --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,33 @@ +# ADR-NNNN: 제목 + +- Status: Proposed +- Date: YYYY-MM-DD +- Decision owners: 이름 또는 팀 +- Related issues: #NN + +## Context + +해결하려는 문제와 제약조건을 작성합니다. + +## Decision + +선택한 방법과 적용 범위를 작성합니다. + +## Alternatives + +- 대안 A와 선택하지 않은 이유 +- 대안 B와 선택하지 않은 이유 + +## Consequences + +### Positive + +- + +### Negative + +- + +## Validation + +결정이 유효한지 확인할 지표와 재검토 시점을 작성합니다. diff --git a/docs/adr/0001-github-flow-monorepo.md b/docs/adr/0001-github-flow-monorepo.md new file mode 100644 index 0000000..2c0468c --- /dev/null +++ b/docs/adr/0001-github-flow-monorepo.md @@ -0,0 +1,45 @@ +# ADR-0001: GitHub Flow 기반 모노레포 운영 + +- Status: Accepted +- Date: 2026-07-16 +- Decision owners: FOWOCO 14조 +- Related issues: #9 + +## Context + +7명이 짧은 프로젝트 기간에 Client, Server, AI, Knowledge, Infra를 병렬 개발해야 합니다. +장기 `develop` 브랜치를 두면 `main`과의 동기화, 릴리스 브랜치 관리, 충돌 해결 비용이 증가합니다. + +## Decision + +- `main`을 유일한 통합 기준으로 사용 +- Issue 기반의 짧은 기능 브랜치와 Draft PR 사용 +- 모노레포 안에서 패키지별 책임을 분리하고 계약으로 연결 +- CI 통과와 리뷰 후 Squash 또는 Rebase 방식으로 병합 +- 병합 후 기능 브랜치 자동 삭제 + +## Alternatives + +- Git Flow: 릴리스가 여러 개 병행되지 않는 학생 프로젝트에 관리 비용이 큼 +- 패키지별 다중 저장소: 초기 계약 변경이 잦아 동기화와 Issue 추적이 어려움 +- `main` 직접 커밋: 변경 추적과 리뷰, CI 게이트를 적용할 수 없음 + +## Consequences + +### Positive + +- 팀 전체 변경과 연동 상태를 한 PR에서 추적 가능 +- 충돌을 조기에 발견하고 작은 단위로 병합 가능 +- Issue·Milestone·CI를 하나의 프로젝트 흐름으로 사용 가능 + +### Negative + +- 패키지 경계를 지키지 않으면 큰 PR이 생길 수 있음 +- 경로별 CI와 명시적인 API·Schema 계약 관리가 필요 + +## Validation + +- PR 평균 리드타임 3일 이하 +- 기능 PR의 변경 목적 1개 유지 +- `main` CI 실패 병합 0건 +- 패키지 간 계약 오류를 통합 전 CI에서 탐지 diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..2f3dbfb --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,12 @@ +# Architecture Decision Records + +ADR은 되돌리기 어렵거나 여러 패키지에 영향을 주는 기술 결정을 기록합니다. + +## 작성 규칙 + +1. `NNNN-short-title.md` 형식으로 번호 증가 +2. 상태는 `Proposed`, `Accepted`, `Superseded`, `Deprecated` 중 하나 +3. 선택한 안뿐 아니라 대안과 선택하지 않은 이유 기록 +4. 결정 변경 시 기존 문서를 삭제하지 않고 새 ADR에서 대체 관계 표시 + +새 문서는 [`0000-template.md`](0000-template.md)를 복사해 작성합니다. From 8c5274a10a819b892dec452b18fced3a409492f0 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 09:24:36 +0900 Subject: [PATCH 7/9] =?UTF-8?q?ci(repo):=20PR=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EA=B3=BC=20=EC=BD=94=EB=93=9C=20=ED=92=88=EC=A7=88=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #9 --- .github/dependabot.yml | 33 +++++++++++++++++++ .github/workflows/knowledge-ci.yml | 4 +++ .github/workflows/pr-conventions.yml | 27 +++++++++++++++ Makefile | 12 +++++-- fowoco-knowledge/pyproject.toml | 11 +++++++ .../src/fowoco_knowledge/__main__.py | 1 - fowoco-knowledge/src/fowoco_knowledge/cli.py | 6 ++-- .../src/fowoco_knowledge/engine.py | 4 ++- .../src/fowoco_knowledge/repository.py | 11 ++----- .../src/fowoco_knowledge/validation.py | 30 ++++++++--------- fowoco-knowledge/tests/conftest.py | 1 - fowoco-knowledge/tests/test_engine.py | 1 - fowoco-knowledge/tests/test_repository.py | 1 - fowoco-knowledge/tests/test_validation.py | 9 ++--- 14 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pr-conventions.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cdc0a21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: /fowoco-knowledge + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Asia/Seoul + open-pull-requests-limit: 5 + labels: + - area:knowledge + - type:chore + - priority:P2 + commit-message: + prefix: chore + include: scope + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Asia/Seoul + open-pull-requests-limit: 3 + labels: + - area:platform + - type:chore + - priority:P2 + commit-message: + prefix: chore + include: scope diff --git a/.github/workflows/knowledge-ci.yml b/.github/workflows/knowledge-ci.yml index 84d8441..c0dac95 100644 --- a/.github/workflows/knowledge-ci.yml +++ b/.github/workflows/knowledge-ci.yml @@ -29,6 +29,10 @@ jobs: cache-dependency-path: fowoco-knowledge/pyproject.toml - name: Install package run: python -m pip install -e "./fowoco-knowledge[dev]" + - name: Lint and format check + run: | + python -m ruff format --check fowoco-knowledge/src fowoco-knowledge/tests + python -m ruff check fowoco-knowledge/src fowoco-knowledge/tests - name: Validate context pack run: python -m fowoco_knowledge validate - name: Run tests diff --git a/.github/workflows/pr-conventions.yml b/.github/workflows/pr-conventions.yml new file mode 100644 index 0000000..9e582b7 --- /dev/null +++ b/.github/workflows/pr-conventions.yml @@ -0,0 +1,27 @@ +name: PR Conventions + +on: + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + title: + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - name: Validate PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + pattern='^(feat|fix|data|docs|test|refactor|chore|experiment|ci|perf|revert)(\([a-z0-9-]+\))?: .+' + if [[ ! "$PR_TITLE" =~ $pattern ]]; then + echo "PR 제목은 (): <한국어 요약> 형식이어야 합니다." + exit 1 + fi + if ! printf '%s' "$PR_TITLE" | grep -q '[가-힣]'; then + echo "콜론 뒤 요약에 한국어를 포함해야 합니다." + exit 1 + fi diff --git a/Makefile b/Makefile index f2a6340..e863e54 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: venv install validate test check +.PHONY: venv install format lint validate test check PYTHON ?= .venv/bin/python BOOTSTRAP_PYTHON ?= python3.11 @@ -9,10 +9,18 @@ venv: install: $(PYTHON) -m pip install -e "./fowoco-knowledge[dev]" +format: + $(PYTHON) -m ruff format fowoco-knowledge/src fowoco-knowledge/tests + $(PYTHON) -m ruff check --fix fowoco-knowledge/src fowoco-knowledge/tests + +lint: + $(PYTHON) -m ruff format --check fowoco-knowledge/src fowoco-knowledge/tests + $(PYTHON) -m ruff check fowoco-knowledge/src fowoco-knowledge/tests + validate: $(PYTHON) -m fowoco_knowledge validate test: $(PYTHON) -m pytest fowoco-knowledge/tests -check: validate test +check: lint validate test diff --git a/fowoco-knowledge/pyproject.toml b/fowoco-knowledge/pyproject.toml index 51a0354..b987b7b 100644 --- a/fowoco-knowledge/pyproject.toml +++ b/fowoco-knowledge/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ dev = [ "pytest>=8.3,<9", "pytest-cov>=6,<7", + "ruff>=0.12,<1", ] [project.scripts] @@ -32,3 +33,13 @@ testpaths = ["tests"] [tool.coverage.run] source = ["fowoco_knowledge"] + +[tool.ruff] +target-version = "py311" +line-length = 100 + +[tool.ruff.lint] +select = ["B", "E", "F", "I", "UP"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101"] diff --git a/fowoco-knowledge/src/fowoco_knowledge/__main__.py b/fowoco-knowledge/src/fowoco_knowledge/__main__.py index a049ad7..bfdcd0c 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/__main__.py +++ b/fowoco-knowledge/src/fowoco_knowledge/__main__.py @@ -1,5 +1,4 @@ from .cli import main - if __name__ == "__main__": raise SystemExit(main()) diff --git a/fowoco-knowledge/src/fowoco_knowledge/cli.py b/fowoco-knowledge/src/fowoco_knowledge/cli.py index 9bfb70e..5a292c9 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/cli.py +++ b/fowoco-knowledge/src/fowoco_knowledge/cli.py @@ -2,8 +2,8 @@ import argparse import json +from collections.abc import Sequence from pathlib import Path -from typing import Sequence from .engine import RequestEvaluator from .repository import KnowledgeRepository @@ -53,7 +53,9 @@ def main(argv: Sequence[str] | None = None) -> int: return 0 if args.command == "compile-context": - print(json.dumps(repository.compile_context(args.workflow_id), ensure_ascii=False, indent=2)) + print( + json.dumps(repository.compile_context(args.workflow_id), ensure_ascii=False, indent=2) + ) return 0 if args.command == "check-request": diff --git a/fowoco-knowledge/src/fowoco_knowledge/engine.py b/fowoco-knowledge/src/fowoco_knowledge/engine.py index 5d63b07..ec63ff0 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/engine.py +++ b/fowoco-knowledge/src/fowoco_knowledge/engine.py @@ -50,7 +50,9 @@ def __init__(self, repository: KnowledgeRepository) -> None: self.schema_validator = Draft202012Validator(schema) def evaluate(self, request: dict[str, Any]) -> EvaluationResult: - schema_errors = sorted(self.schema_validator.iter_errors(request), key=lambda e: list(e.path)) + schema_errors = sorted( + self.schema_validator.iter_errors(request), key=lambda e: list(e.path) + ) if schema_errors: message = "; ".join(error.message for error in schema_errors) raise ValueError(f"Invalid agent request: {message}") diff --git a/fowoco-knowledge/src/fowoco_knowledge/repository.py b/fowoco-knowledge/src/fowoco_knowledge/repository.py index 281ae17..f5f2c75 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/repository.py +++ b/fowoco-knowledge/src/fowoco_knowledge/repository.py @@ -37,8 +37,7 @@ def discover_project_root(explicit_root: str | Path | None = None) -> Path: return resolved raise KnowledgeNotFoundError( - "FOWOCO knowledge root를 찾지 못했습니다. " - "--root 또는 FOWOCO_KNOWLEDGE_ROOT를 지정하세요." + "FOWOCO knowledge root를 찾지 못했습니다. --root 또는 FOWOCO_KNOWLEDGE_ROOT를 지정하세요." ) @@ -96,15 +95,11 @@ def compile_context(self, workflow_id: str) -> dict[str, Any]: item for item in context["intents"]["intents"] if item["id"] == workflow["intent"] ) domains = [ - item - for item in context["domains"]["domains"] - if item["id"] in workflow["domains"] + item for item in context["domains"]["domains"] if item["id"] in workflow["domains"] ] slot_policy = context["slots"]["workflow_requirements"][workflow["required_slots_ref"]] sources = [ - item - for item in context["sources"]["sources"] - if item["id"] in workflow["source_ids"] + item for item in context["sources"]["sources"] if item["id"] in workflow["source_ids"] ] guardrails = [ item diff --git a/fowoco-knowledge/src/fowoco_knowledge/validation.py b/fowoco-knowledge/src/fowoco_knowledge/validation.py index 75e07a9..d5d17b1 100644 --- a/fowoco-knowledge/src/fowoco_knowledge/validation.py +++ b/fowoco-knowledge/src/fowoco_knowledge/validation.py @@ -2,14 +2,12 @@ import csv import json -from pathlib import Path from typing import Any from jsonschema import Draft202012Validator from .repository import KnowledgeRepository - SEED_COLUMNS = { "request_id", "source", @@ -99,9 +97,7 @@ def _validate_cross_references(self) -> None: document_types = set(context["checklists"]["document_types"]) for checklist_id, checklist in checklists.items(): if checklist["workflow_id"] not in workflows: - self.errors.append( - f"{checklist_id}: unknown workflow {checklist['workflow_id']}" - ) + self.errors.append(f"{checklist_id}: unknown workflow {checklist['workflow_id']}") for item in checklist["items"]: if item["document_type"] not in document_types: self.errors.append( @@ -113,9 +109,7 @@ def _validate_cross_references(self) -> None: for template in context["multilingual_templates"]["templates"]: if template["workflow_id"] not in workflows: - self.errors.append( - f"{template['id']}: unknown workflow {template['workflow_id']}" - ) + self.errors.append(f"{template['id']}: unknown workflow {template['workflow_id']}") valid_guardrail_targets = set(intents) | {"ALL"} for rule in context["guardrails"]["rules"]: @@ -131,9 +125,7 @@ def _validate_seed_data(self) -> None: known_domains = {item["id"] for item in context["domains"]["domains"]} known_workflows = {item["id"] for item in context["workflows"]["workflows"]} known_sources = set(self.repository.load_yaml("data/provenance.yaml")["sources"]) - review_statuses = set( - self.repository.load_yaml("data/provenance.yaml")["review_statuses"] - ) + review_statuses = set(self.repository.load_yaml("data/provenance.yaml")["review_statuses"]) input_modes = set(self.repository.manifest["input_modes"]) path = self.repository.root / "data/seed/gold_seed.csv" @@ -185,7 +177,9 @@ def _validate_evaluation_data(self) -> None: known_workflows = {item["id"] for item in context["workflows"]["workflows"]} seen: set[str] = set() path = self.repository.root / "data/evaluation/golden_cases.jsonl" - for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + for line_number, raw_line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): if not raw_line.strip(): continue try: @@ -199,8 +193,12 @@ def _validate_evaluation_data(self) -> None: if case_id in seen: self.errors.append(f"evaluation line {line_number}: duplicate {case_id}") seen.add(case_id) - self._check_codes(line_number, "intent", case.get("expected_intents", []), known_intents) - self._check_codes(line_number, "domain", case.get("expected_domains", []), known_domains) + self._check_codes( + line_number, "intent", case.get("expected_intents", []), known_intents + ) + self._check_codes( + line_number, "domain", case.get("expected_domains", []), known_domains + ) self._check_codes( line_number, "workflow", @@ -217,9 +215,7 @@ def _index_unique(self, items: list[dict[str, Any]], kind: str) -> dict[str, dic indexed[item_id] = item return indexed - def _check_codes( - self, line_number: int, kind: str, values: list[str], known: set[str] - ) -> None: + def _check_codes(self, line_number: int, kind: str, values: list[str], known: set[str]) -> None: for value in values: if value not in known: self.errors.append(f"line {line_number}: unknown {kind} {value}") diff --git a/fowoco-knowledge/tests/conftest.py b/fowoco-knowledge/tests/conftest.py index 567e8e0..6a6766c 100644 --- a/fowoco-knowledge/tests/conftest.py +++ b/fowoco-knowledge/tests/conftest.py @@ -3,7 +3,6 @@ import sys from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parents[1] SRC = PROJECT_ROOT / "src" if str(SRC) not in sys.path: diff --git a/fowoco-knowledge/tests/test_engine.py b/fowoco-knowledge/tests/test_engine.py index b6bed40..031a152 100644 --- a/fowoco-knowledge/tests/test_engine.py +++ b/fowoco-knowledge/tests/test_engine.py @@ -6,7 +6,6 @@ from fowoco_knowledge.engine import RequestEvaluator from fowoco_knowledge.repository import KnowledgeRepository - ROOT = Path(__file__).resolve().parents[1] diff --git a/fowoco-knowledge/tests/test_repository.py b/fowoco-knowledge/tests/test_repository.py index 1a472c0..d220792 100644 --- a/fowoco-knowledge/tests/test_repository.py +++ b/fowoco-knowledge/tests/test_repository.py @@ -4,7 +4,6 @@ from fowoco_knowledge.repository import KnowledgeRepository - ROOT = Path(__file__).resolve().parents[1] diff --git a/fowoco-knowledge/tests/test_validation.py b/fowoco-knowledge/tests/test_validation.py index d576c09..db32571 100644 --- a/fowoco-knowledge/tests/test_validation.py +++ b/fowoco-knowledge/tests/test_validation.py @@ -8,7 +8,6 @@ from fowoco_knowledge.repository import KnowledgeRepository from fowoco_knowledge.validation import KnowledgeValidator, split_codes - ROOT = Path(__file__).resolve().parents[1] @@ -18,15 +17,11 @@ def test_all_knowledge_files_are_valid() -> None: def test_seed_has_coverage_but_is_not_claimed_as_training_ready() -> None: - with (ROOT / "data/seed/gold_seed.csv").open( - "r", encoding="utf-8-sig", newline="" - ) as handle: + with (ROOT / "data/seed/gold_seed.csv").open("r", encoding="utf-8-sig", newline="") as handle: rows = list(csv.DictReader(handle)) assert len(rows) == 40 - intent_counts = Counter( - intent for row in rows for intent in split_codes(row["intents"]) - ) + intent_counts = Counter(intent for row in rows for intent in split_codes(row["intents"])) for intent in { "WORKER_ONBOARDING", "EXPIRY_RENEWAL", From 9d7f279bf9e9d2dc1e2474e63605377ba1b35f76 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 09:26:24 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix(ci):=20=ED=95=9C=EA=B8=80=20PR=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EA=B2=80=EC=82=AC=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #9 --- .github/workflows/pr-conventions.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-conventions.yml b/.github/workflows/pr-conventions.yml index 9e582b7..3cda5f7 100644 --- a/.github/workflows/pr-conventions.yml +++ b/.github/workflows/pr-conventions.yml @@ -16,12 +16,18 @@ jobs: env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - pattern='^(feat|fix|data|docs|test|refactor|chore|experiment|ci|perf|revert)(\([a-z0-9-]+\))?: .+' - if [[ ! "$PR_TITLE" =~ $pattern ]]; then - echo "PR 제목은 (): <한국어 요약> 형식이어야 합니다." - exit 1 - fi - if ! printf '%s' "$PR_TITLE" | grep -q '[가-힣]'; then - echo "콜론 뒤 요약에 한국어를 포함해야 합니다." - exit 1 - fi + python - <<'PY' + import os + import re + import sys + + title = os.environ["PR_TITLE"] + pattern = r"^(feat|fix|data|docs|test|refactor|chore|experiment|ci|perf|revert)(\([a-z0-9-]+\))?: .+" + + if not re.fullmatch(pattern, title): + print("PR 제목은 (): <한국어 요약> 형식이어야 합니다.") + sys.exit(1) + if not re.search(r"[가-힣]", title): + print("콜론 뒤 요약에 한국어를 포함해야 합니다.") + sys.exit(1) + PY From 8aa9309f85a1e7e8448b374d6619147a3404bbea Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 16 Jul 2026 09:31:37 +0900 Subject: [PATCH 9/9] =?UTF-8?q?data(knowledge):=20=EC=A0=84=EB=AC=B8?= =?UTF-8?q?=EA=B0=80=20=EA=B2=80=EC=88=98=EC=99=80=20Gold=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=EA=B8=B0=EC=A4=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #10 --- .github/ISSUE_TEMPLATE/knowledge-change.yml | 10 +++ fowoco-knowledge/data/provenance.yaml | 22 +++++- .../data/review/expert_review_template.csv | 1 + .../data/review/team_annotation_template.csv | 1 + fowoco-knowledge/docs/DATA_GUIDE.md | 19 +++-- fowoco-knowledge/docs/EXPERT_REVIEW.md | 79 +++++++++++++++++++ 6 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 fowoco-knowledge/data/review/expert_review_template.csv create mode 100644 fowoco-knowledge/data/review/team_annotation_template.csv create mode 100644 fowoco-knowledge/docs/EXPERT_REVIEW.md diff --git a/.github/ISSUE_TEMPLATE/knowledge-change.yml b/.github/ISSUE_TEMPLATE/knowledge-change.yml index bbc8051..cd382ba 100644 --- a/.github/ISSUE_TEMPLATE/knowledge-change.yml +++ b/.github/ISSUE_TEMPLATE/knowledge-change.yml @@ -42,6 +42,16 @@ body: label: 변경 내용과 근거 validations: required: true + - type: dropdown + id: expert_review + attributes: + label: 전문가 검수 수준 + options: + - 고위험·공식절차 직접 검수 필요 + - low·medium 층화표본 검수 필요 + - 기술·문서 변경으로 전문가 검수 불필요 + validations: + required: true - type: checkboxes id: safety attributes: diff --git a/fowoco-knowledge/data/provenance.yaml b/fowoco-knowledge/data/provenance.yaml index fadbe8a..8779616 100644 --- a/fowoco-knowledge/data/provenance.yaml +++ b/fowoco-knowledge/data/provenance.yaml @@ -17,5 +17,25 @@ sources: review_statuses: - DRAFT - REVIEWED_ONCE - - GOLD + - TEAM_AGREED + - EXPERT_REVIEW_REQUIRED + - GOLD_TEAM + - GOLD_EXPERT - REJECTED +gold_policy: + GOLD_TEAM: + allowed_risk: [low, medium] + requirements: + - 두 팀원이 서로의 결과를 보지 않고 독립 라벨링 + - 불일치 합의와 검수 메모 기록 + - 해당 Intent의 전문가 층화표본 검수 통과 + GOLD_EXPERT: + allowed_risk: [low, medium, high, critical] + requirements: + - GOLD_TEAM 요구사항 충족 + - 도메인 전문가가 해당 문장 또는 Workflow를 직접 검수 + mandatory_expert_review: + - high 또는 critical 민감도 + - 두 팀원의 Intent·Domain·Workflow 불일치 + - 법령·필수서류·기관 절차를 정답으로 포함한 사례 + - 전문가 표본 검수에서 수정된 유형 diff --git a/fowoco-knowledge/data/review/expert_review_template.csv b/fowoco-knowledge/data/review/expert_review_template.csv new file mode 100644 index 0000000..3886397 --- /dev/null +++ b/fowoco-knowledge/data/review/expert_review_template.csv @@ -0,0 +1 @@ +review_id,artifact_type,artifact_id,expert_role,review_scope,risk_level,decision,correction_summary,source_reference,reviewed_at diff --git a/fowoco-knowledge/data/review/team_annotation_template.csv b/fowoco-knowledge/data/review/team_annotation_template.csv new file mode 100644 index 0000000..f60baba --- /dev/null +++ b/fowoco-knowledge/data/review/team_annotation_template.csv @@ -0,0 +1 @@ +request_id,reviewer_code,intents,domains,workflow_ids,slots_json,missing_slots,ambiguities,sensitivity,next_action,decision,notes,reviewed_at diff --git a/fowoco-knowledge/docs/DATA_GUIDE.md b/fowoco-knowledge/docs/DATA_GUIDE.md index 5e2a7d1..7e4b1b2 100644 --- a/fowoco-knowledge/docs/DATA_GUIDE.md +++ b/fowoco-knowledge/docs/DATA_GUIDE.md @@ -23,16 +23,21 @@ | `sensitivity` | `low`, `medium`, `high`, `critical` | | `next_action` | 추가질문, HR 검토, 후보 분리 등 시스템의 다음 행동 | | `expected_output` | 화면에 생성되어야 할 산출물 | -| `review_status` | `DRAFT` → `REVIEWED_ONCE` → `GOLD` | +| `review_status` | `DRAFT` → `REVIEWED_ONCE` → `TEAM_AGREED` → `GOLD_TEAM` 또는 `GOLD_EXPERT` | ## 작성·검수 순서 1. 작성자가 한 행 작성 -2. 검수자 A가 Intent·Domain·Workflow 확인 -3. 검수자 B가 Slot·누락·모호성 확인 -4. 불일치는 회의에서 기준을 정하고 가이드 갱신 -5. 합의된 행만 `GOLD`로 변경 -6. 평가용 사례는 별도 파일로 이동하고 이후 프롬프트 예시에 사용하지 않음 +2. 검수자 A와 B가 서로의 답을 보지 않고 별도 양식에 전체 라벨 작성 +3. Intent·Domain은 Cohen's Kappa, Slot은 필드 Exact Match로 사전 일치도 확인 +4. 불일치는 회의에서 합의하고 라벨 가이드 갱신 +5. low·medium은 해당 Intent의 전문가 층화표본 검수가 통과해야 `GOLD_TEAM` 승격 +6. high·critical, 팀 불일치, 법령·서류·기관절차 사례는 전문가가 직접 확인해야 `GOLD_EXPERT` 승격 +7. 평가용 사례는 별도 파일로 이동하고 이후 프롬프트 예시에 사용하지 않음 + +팀 내부 운영 기준으로 Intent·Domain Kappa가 0.70 미만이면 라벨 가이드를 먼저 수정합니다. +전문가 표본의 수정·거절률이 10%를 넘으면 해당 Intent 또는 Workflow 전체를 전문가 재검수합니다. +이 수치는 보편적인 학술 기준이 아니라 FOWOCO의 초기 품질 게이트입니다. ## 현실적인 수량 단계 @@ -53,3 +58,5 @@ - 합성 문장을 실제 기업 데이터로 표기 - 평가 문장을 학습셋이나 Few-shot 예시에 재사용 - 공공 민원 Intent를 FOWOCO Intent 정답으로 그대로 치환 + +전문가 검수 범위와 양식은 [`EXPERT_REVIEW.md`](EXPERT_REVIEW.md)를 따릅니다. diff --git a/fowoco-knowledge/docs/EXPERT_REVIEW.md b/fowoco-knowledge/docs/EXPERT_REVIEW.md new file mode 100644 index 0000000..2af765e --- /dev/null +++ b/fowoco-knowledge/docs/EXPERT_REVIEW.md @@ -0,0 +1,79 @@ +# 전문가 검수 운영 기준 + +## 왜 필요한가 + +팀원 두 명의 독립 검수는 **라벨 기준의 일관성**을 확인합니다. +그러나 다음 내용이 실제 제도·현장과 맞는지는 도메인 경험 없이 판단할 수 없습니다. + +- E-9 체류·계약·고용변동 Workflow 순서 +- 실제로 요청하는 서류명과 조건 +- 급여·공제·퇴사 안내의 위험표현 +- 외국인근로자가 이해하기 어려운 한국어 표현 + +따라서 팀 검수와 전문가 검수는 서로 대체하지 않고 목적을 분리합니다. + +## 전문가 역할 + +| 검수 영역 | 적합한 검수자 | 확인 내용 | +| --- | --- | --- | +| E-9 행정 Workflow | E-9 제조업 HR 실무자, 외국인력 지원기관 실무자 | 업무 순서, 실제 담당자, 반복 여부, 완료 증빙 | +| 노무·급여·고용변동 | 공인노무사 또는 노무·급여 실무자 | 위험표현, 계산과 설명의 경계, 전문가 이관 조건 | +| 쉬운 한국어·다국어 안내 | E-9 교육 담당자, 이중언어 강사·검수자 | 이해하기 어려운 용어, 행동 지시의 명확성 | + +인터뷰 참여자를 자동으로 전문가라 부르지 않습니다. 검수자의 역할, 관련 경험, 검수 범위가 +해당 영역과 맞을 때만 전문가 검수로 기록합니다. + +## 현실적인 검수 범위 + +### 100% 전문가 검수 + +- `high`, `critical` 민감도 +- 두 팀원의 Intent·Domain·Workflow 불일치 +- 법령, 필수서류, 기관 신고 절차를 정답으로 포함한 사례 +- 급여 지연, 공제, 퇴사, 이탈 등 분쟁 가능 표현 + +### 층화표본 검수 + +- low·medium 사례는 Intent별 최소 20% +- 단일·복합·모호·범위 밖 사례를 각각 포함 +- 같은 문장 구조가 반복되지 않도록 표현군 단위로 추출 + +표본의 수정·거절률이 10%를 넘으면 해당 Intent 또는 Workflow 전체를 재검수합니다. + +## 검수 절차 + +```text +팀원 A·B 독립 라벨링 + -> 사전 일치도 계산 + -> 불일치 합의 + -> 고위험·표본 추출 + -> 전문가 O / 수정 / X 검수 + -> 라벨 가이드·Context Pack 반영 + -> GOLD_TEAM 또는 GOLD_EXPERT 승격 +``` + +## 판정 + +| Decision | 의미 | +| --- | --- | +| `ACCEPT` | 실제 업무와 라벨이 적절함 | +| `ACCEPT_WITH_EDIT` | 수정 후 사용 가능 | +| `REJECT` | 실제 업무와 맞지 않거나 위험함 | +| `OUT_OF_SCOPE` | 해당 전문가의 검수 범위를 벗어남 | + +## 기록 원칙 + +- 검수자의 실명은 동의 없이 저장하지 않고 `E9_HR_PRACTITIONER` 같은 역할 코드 사용 +- 회사·근로자 개인정보와 원본 문서는 전달하지 않음 +- 검수일, 대상 ID, 범위, 판정, 수정 요약, 근거 출처를 기록 +- 전문가 의견이 공식자료와 충돌하면 공식기관에 재확인하고 근거를 함께 남김 +- 전문가 1인의 의견을 전체 산업의 수요 근거로 일반화하지 않음 + +## MVP 최소 기준 + +1. E-9 행정 Workflow 검수자 1명 +2. 제조업 한국어·다국어 안내 검수자 1명 +3. 급여·고용변동을 MVP에 포함한다면 노무·급여 전문가 1명 + +세 번째 검수자를 확보하지 못하면 급여·고용변동은 자동 답변이 아니라 HR 검토·전문가 이관 +Workflow로만 시연합니다.