Skip to content

Feat(#36): 형태소·문자 기반 피싱 분류 모델 추가 및 성능 비교 (1/2) - #38

Merged
pearseona merged 12 commits into
developfrom
feat/36-compare-phishing-models
Aug 9, 2026
Merged

Feat(#36): 형태소·문자 기반 피싱 분류 모델 추가 및 성능 비교 (1/2)#38
pearseona merged 12 commits into
developfrom
feat/36-compare-phishing-models

Conversation

@pearseona

@pearseona pearseona commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📝 개요

이 PR은 이슈 #36을 두 개의 PR로 나누어 진행하는 첫 번째 PR입니다.

기존 Naive Bayes 모델과 형태소 기반 Logistic Regression, 문자 n-gram
기반 Linear SVM을 동일한 조건에서 비교하기 위해서는 먼저 재현 가능한
데이터 분할과 공통 평가 기반이 필요합니다.

Logistic Regression, Linear SVM 및 최종 모델 비교는 후속 PR (2/2)에서
구현할 예정입니다.

🔗 관련 이슈

🎯 주요 변경 사항

1. 공통 SMS 전처리

  • 학습 코드와 API의 텍스트 정규화 로직 공통화
  • URL, 전화번호, 계좌번호, 금액 등 마스킹 규칙 통일
  • 구조 피처 추출 로직 공통화
  • 기존 Naive Bayes API 회귀 테스트 추가

2. 중복·유사 메시지 그룹화

  • 정규화 텍스트 fingerprint 생성
  • 완전 중복 메시지 제거
  • 문자 n-gram 기반 유사도 계산
  • 유사 메시지에 동일한 template_group_id 부여
  • 그룹화 임계값과 설정 분리
  • 그룹화 단위 테스트 추가

3. 데이터 누수 방지 분할

  • template_group_id 단위 train/validation/test 분할
  • split 비율과 random seed 고정
  • 클래스 비율을 최대한 보존
  • split manifest 생성
  • 그룹 및 fingerprint 교차 검증

생성 파일:

  • data_science/SMSModel/splits/sms_split_v1.csv

4. 데이터 분할 검증 보고서

  • split별 전체 건수
  • normal/phishing 비율
  • 메시지 유형별 분포
  • 그룹 및 fingerprint 교차 여부
  • 데이터 fingerprint
  • 유사도 임계값
  • 검증 실패 시 학습 중단

생성 파일:

  • data_science/SMSModel/reports/dataset_split_summary.json
  • data_science/SMSModel/reports/dataset_split_summary.md

5. 공통 모델 평가 파이프라인

다음 공통 인터페이스와 평가 기능을 추가했습니다.

  • fit
  • predict
  • predict_scores
  • probability/decision score 구분
  • Precision, Recall, F1, F2
  • confusion matrix
  • False Negative 개수
  • 평균 및 P95 단건 추론 시간
  • validation 기반 threshold 선택
  • JSON/CSV/Markdown 평가 보고서

6. 기존 Naive Bayes baseline 연결

  • 기존 ComplementNB 설정 유지
  • 기존 구조 피처 포함
  • leakage-safe split 사용
  • validation set에서 threshold 선택
  • text-only와 structural 모델 결과 구분
  • structural 모델을 공식 baseline으로 표시
  • 기존 운영 artifact와 API 동작 유지

평가 결과:

Model Threshold Precision Recall F1 F2 FN
NB text-only 0.063846 0.5447 1.0000 0.7053 0.8568 0
NB structural 0.056923 0.5447 1.0000 0.7053 0.8568 0

평가 보고서:

  • data_science/SMSModel/reports/model_evaluation/naive_bayes_baseline/

Baseline 실행은 기존 운영 artifact를 자동으로 교체하지 않습니다.
평가와 운영 모델 배포를 분리하여 기존 API 동작을 보존합니다.

7. Kiwi 형태소 tokenizer

  • kiwipiepy==0.23.2 의존성 추가
  • 분류에 사용할 품사 목록 명시
  • 불규칙 활용 품사 접미사 처리
  • [URL], [ACCOUNT] 등 마스킹 토큰 보존
  • 빈 문자열과 특수문자 입력 처리
  • scikit-learn CountVectorizer 연동 테스트
  • joblib 직렬화·복원 테스트
  • Docker 환경 설치 및 초기화 검증

데이터 분할 결과

  • 전체 중복 제거 후 데이터: 817건
  • Train: 571건
  • Validation: 123건
  • Test: 123건
  • Train/Validation/Test 그룹 교차: 0건
  • Train/Validation/Test fingerprint 교차: 0건
  • Dataset fingerprint:
    1db45d5f2c3d3d17726888b05cd625e0d0a51deef3dc8ab94016a9ee97af18f5

📸 사진

✅ PR 체크리스트

  • 관련 이슈를 연결했습니다.
  • 구현 범위와 변경 이유를 설명했습니다.
  • 로컬 테스트(uvicorn 구동 또는 테스트 코드)를 통과했습니다.
  • API 변경 사항이 있다면 Swagger / API 명세에 반영했습니다.
  • 민감 정보(API Key, 시크릿 키 등)가 코드·로그·테스트 데이터에 포함되지 않았습니다.
  • 프론트엔드 또는 메인 백엔드(Spring)에 영향을 주는 응답 스키마 또는 Enum 변경이 있다면 팀에 공유했습니다.
  • 병합(Merge) 전 작업 브랜치를 삭제하지 않았습니다.

Summary by CodeRabbit

  • New Features
    • Added reusable SMS preprocessing, Korean tokenization, template grouping, and leakage-safe dataset splitting.
    • Added phishing model training, threshold selection, evaluation metrics, latency measurement, and JSON/CSV/Markdown reports.
    • Added reproducible dataset manifests and split summary reports.
  • Bug Fixes
    • Updated model artifact paths and strengthened model loading and fallback behavior.
  • Documentation
    • Added SMS model workspace and reproducibility documentation.
  • Tests
    • Expanded coverage across preprocessing, modeling, evaluation, tokenization, grouping, splitting, and reporting.

@pearseona pearseona self-assigned this Aug 7, 2026
@pearseona pearseona added the feat New feature or functional additions to the application label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pearseona, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5501cad1-c9c8-4a98-b410-5e39cf9f3000

📥 Commits

Reviewing files that changed from the base of the PR and between bdf74a4 and dd40ea9.

📒 Files selected for processing (6)
  • Dockerfile
  • app/analysis/scoring.py
  • data_science/SMSModel/README.md
  • data_science/SMSModel/SMSDataModel.ipynb
  • data_science/SMSModel/train_sms.py
  • tests/analysis/test_service.py
📝 Walkthrough

Walkthrough

This PR adds shared SMS preprocessing, Kiwi tokenization, leakage-safe grouped splits, Naive Bayes evaluation, reporting, versioned artifact export, and updated model paths. It also reformats unrelated application and test files without changing their behavior.

Changes

SMS model pipeline

Layer / File(s) Summary
Shared preprocessing and inference
app/analysis/text/*, data_science/SMSModel/tokenization/*
Centralizes text normalization and six structural features. Adds Kiwi tokenization. Updates analyzer loading, inference, fallback handling, and versioned artifact resolution.
Template grouping and reproducible splits
data_science/SMSModel/template_grouping/*, data_science/SMSModel/dataset_splitting/*, data_science/SMSModel/train_sms.py
Adds fingerprints, duplicate validation, similarity groups, grouped splits, manifests, split validation, and training integration.
Naive Bayes modeling and evaluation
data_science/SMSModel/modeling/*, data_science/SMSModel/evaluation/*
Adds classifier contracts, structural ComplementNB models, threshold selection, metrics, latency measurement, report writers, and operational artifact export.
Reports and baseline orchestration
data_science/SMSModel/reporting/*, data_science/SMSModel/run_naive_bayes_baseline.py, data_science/SMSModel/reports/*
Adds dataset and model evaluation reports, committed report outputs, and a runner for text-only and structural models.
Deployment and project support
Dockerfile, .dockerignore, .env.example, app/core/config.py, app/main.py, requirements.txt, pytest.ini, data_science/SMSModel/README.md
Updates artifact paths, validates Kiwi during image builds, adds dependencies and a test marker, documents the SMS workspace, and resolves active artifacts during application startup.
Unrelated formatting updates
app/analysis/*, app/chat/*, app/infrastructure/*, data_science/VoiceModel/*, tests/analysis/*, tests/chat/*, tests/infrastructure/*, tests/integration/*
Reformats imports, annotations, expressions, notebook cells, logging calls, and test setup without changing observable behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • SafeFam/SafeFam_AI#20 — Directly related to the Naive Bayes scoring flow extended by this PR.
  • SafeFam/SafeFam_AI#24 — Introduced the analyzer that this PR extends with shared preprocessing and versioned artifact loading.
  • SafeFam/SafeFam_AI#35 — Related to the Naive Bayes artifact paths and Docker configuration updated here.

Sequence Diagram(s)

sequenceDiagram
  participant TrainingPipeline
  participant TemplateGrouping
  participant DatasetSplitter
  participant Evaluator
  participant ArtifactStore
  participant SMSAnalyzer
  TrainingPipeline->>TemplateGrouping: fingerprint, deduplicate, and group SMS messages
  TemplateGrouping->>DatasetSplitter: provide grouped dataset
  DatasetSplitter-->>TrainingPipeline: return validated train, validation, and test splits
  TrainingPipeline->>Evaluator: train models and select validation thresholds
  Evaluator->>ArtifactStore: save versioned model and vectorizer artifacts
  SMSAnalyzer->>ArtifactStore: resolve current.json and load active artifacts
  SMSAnalyzer-->>TrainingPipeline: return phishing probability and risk result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the addition of SMS phishing classification components and the Naive Bayes performance comparison covered by this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/36-compare-phishing-models

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
data_science/SMSModel/train_sms.py (2)

235-324: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider failing when the committed manifest is absent.

The README states that the committed manifest fixes the final test set. When SPLIT_MANIFEST_PATH does not exist and create_manifest is False, split_data generates and saves a new manifest anyway. run_naive_bayes_baseline.py calls split_data(dataset) with the same defaults, so baseline metrics can be produced against an ad-hoc test set. The print at Line 274 is the only signal. Require create_manifest=True to create a manifest, so an accidental regeneration stops the run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/SMSModel/train_sms.py` around lines 235 - 324, Update split_data
so that when SPLIT_MANIFEST_PATH is absent and create_manifest is False, it
raises an error instead of generating or saving a new split; only the explicit
create_manifest=True path may call split_grouped_dataset and
save_split_manifest, while the existing manifest-loading path remains unchanged.

141-142: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The null check covers columns that the schema does not require.

required_columns lists only text, label, type, and has_url. df.isnull().any().any() rejects nulls in every column, including the optional source column that Line 160 reads with a default. A dataset that adds any optional column with blank cells fails to load, and the error message does not name the column. Restrict the check to the required columns and report the offending ones.

♻️ Proposed change
-    if df.isnull().any().any():
-        raise ValueError("결측치가 존재합니다.")
+    null_columns = [
+        column
+        for column in required_columns
+        if df[column].isnull().any()
+    ]
+    if null_columns:
+        raise ValueError(f"결측치가 존재하는 컬럼: {sorted(null_columns)}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/SMSModel/train_sms.py` around lines 141 - 142, Update the null
validation in the training-data loading flow to inspect only the columns listed
by required_columns (text, label, type, and has_url), leaving optional columns
such as source eligible for the existing default handling. Collect the required
columns containing nulls and include their names in the ValueError message
instead of reporting a generic failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/analysis/text/naive_bayes_analyzer.py`:
- Around line 103-110: Update the result construction in the analyzer method so
result["error_message"] always uses DEFAULT_ANALYSIS_RESULT["error_message"]
rather than _load_error; retain _load_error only for internal logging where the
exception type belongs.
- Around line 58-61: Update _load_artifacts to synchronize concurrent callers
with a lock: acquire the lock before checking _load_attempted, re-check the flag
after acquiring it, and keep the flag unset until the artifact load completes or
otherwise ensure waiting callers do not return while _model is still
unavailable. Preserve the existing one-time loading behavior and have concurrent
startup requests observe the successfully loaded model.
- Around line 116-119: The single-message feature construction in
naive_bayes_analyzer.py and predict_risk_score does not reproduce the declared
has_url training signal. Update each extract_struct_features call to pass the
same has_url value used by extract_struct_feature_matrix, including the site at
data_science/SMSModel/train_sms.py:546, or consistently derive that value
identically during training and serving.

In `@data_science/SMSModel/dataset_splitting/manifest.py`:
- Around line 14-20: Update the manifest-building and saving flow to accept and
use DatasetSplitConfig, including build_split_manifest and save_split_manifest,
instead of relying on fixed MANIFEST_COLUMNS names. Derive the fingerprint,
group, and label columns from the configuration so renamed key columns work end
to end, and add a round-trip test covering custom column names.

In `@data_science/SMSModel/dataset_splitting/splitter.py`:
- Around line 85-92: Update _select_best_group_split and its
candidate-generation flow so candidates vary across feasible group counts
instead of fixing one count before scoring. Ensure _candidate_score evaluates
each whole-group split against the configured train, validation, and test row
ratios (while preserving label-distribution scoring), allowing unequal group
sizes to be optimized for row-level targets.

In `@data_science/SMSModel/evaluation/threshold.py`:
- Around line 53-58: Update the validation logic in the threshold-selection
function around allowed_labels to reject y_true when it contains only one
distinct supported label, raising ValueError before computing or returning any
score-derived threshold. Preserve the existing unsupported-label validation, and
add coverage for normal-only and phishing-only validation data.

In `@data_science/SMSModel/modeling/artifacts.py`:
- Around line 51-65: The save_operational_naive_bayes_artifacts() flow currently
exposes model_path and vectorizer_path independently, allowing mixed artifact
generations during reloads. Store both outputs in a new versioned directory,
then atomically update a single manifest or pointer only after both files are
successfully written; update the API loader to resolve both artifacts through
that shared pointer.

In `@data_science/SMSModel/modeling/base.py`:
- Around line 28-37: Update __post_init__ to assign the normalized result of
np.asarray(self.values) back to self.values before validation, so list inputs
are stored as NumPy arrays. Preserve the existing dimensionality, finiteness,
and probability-range checks.

In `@data_science/SMSModel/modeling/naive_bayes.py`:
- Around line 210-228: Before constructing or fitting CalibratedClassifierCV in
the training flow, use train_df["label"].value_counts().min() to validate that
every class has at least self.calibration_cv samples. Raise the established
validation error when the minimum count is insufficient, while preserving
_validate_dataframe and the existing calibration setup for valid datasets.

In `@data_science/SMSModel/README.md`:
- Around line 3-13: Add tokenization/, modeling/, evaluation/, and
run_naive_bayes_baseline.py as rows in the workspace table, with concise
purposes matching their roles, so the README inventory includes every newly
added package and baseline runner.

In `@Dockerfile`:
- Around line 13-19: Ensure the runtime image includes the module path required
by the serialized vectorizer’s kiwi_tokenize callable. Update the Dockerfile
COPY steps to include data_science/SMSModel/tokenization/__init__.py and
kiwi_tokenizer.py, or relocate kiwi_tokenize under an already-copied package
while preserving its import path at model load time.

In `@tests/data_science/SMSModel/test_template_grouping.py`:
- Around line 305-307: Rename the unused holdout result binding in the load_data
call to _df_holdout, while preserving df_pool and the existing load_data
invocation.

---

Nitpick comments:
In `@data_science/SMSModel/train_sms.py`:
- Around line 235-324: Update split_data so that when SPLIT_MANIFEST_PATH is
absent and create_manifest is False, it raises an error instead of generating or
saving a new split; only the explicit create_manifest=True path may call
split_grouped_dataset and save_split_manifest, while the existing
manifest-loading path remains unchanged.
- Around line 141-142: Update the null validation in the training-data loading
flow to inspect only the columns listed by required_columns (text, label, type,
and has_url), leaving optional columns such as source eligible for the existing
default handling. Collect the required columns containing nulls and include
their names in the ValueError message instead of reporting a generic failure.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 870ca283-5df5-497c-8d82-5dfa1d9579aa

📥 Commits

Reviewing files that changed from the base of the PR and between cd4515d and 2207354.

⛔ Files ignored due to path filters (8)
  • data_science/SMSModel/artifacts/phishing_model_artifact.pkl is excluded by !**/*.pkl
  • data_science/SMSModel/artifacts/phishing_vectorizer.pkl is excluded by !**/*.pkl
  • data_science/SMSModel/reports/feature_scores_full.csv is excluded by !**/*.csv
  • data_science/SMSModel/reports/figures/feature_importance.png is excluded by !**/*.png
  • data_science/SMSModel/reports/figures/risk_distribution_fig1.png is excluded by !**/*.png
  • data_science/SMSModel/reports/figures/risk_distribution_fig2.png is excluded by !**/*.png
  • data_science/SMSModel/reports/model_evaluation/naive_bayes_baseline/model_evaluation.csv is excluded by !**/*.csv
  • data_science/SMSModel/splits/sms_split_v1.csv is excluded by !**/*.csv
📒 Files selected for processing (62)
  • .dockerignore
  • .env.example
  • Dockerfile
  • SCORING_PIPELINE_CHANGES.md
  • app/analysis/text/naive_bayes_analyzer.py
  • app/analysis/text/preprocessing.py
  • app/core/config.py
  • data_science/SMSModel/README.md
  • data_science/SMSModel/SMSDataModel.ipynb
  • data_science/SMSModel/dataset_splitting/__init__.py
  • data_science/SMSModel/dataset_splitting/config.py
  • data_science/SMSModel/dataset_splitting/manifest.py
  • data_science/SMSModel/dataset_splitting/splitter.py
  • data_science/SMSModel/dataset_splitting/validation.py
  • data_science/SMSModel/evaluation/__init__.py
  • data_science/SMSModel/evaluation/evaluator.py
  • data_science/SMSModel/evaluation/latency.py
  • data_science/SMSModel/evaluation/metrics.py
  • data_science/SMSModel/evaluation/reporting.py
  • data_science/SMSModel/evaluation/threshold.py
  • data_science/SMSModel/modeling/__init__.py
  • data_science/SMSModel/modeling/artifacts.py
  • data_science/SMSModel/modeling/base.py
  • data_science/SMSModel/modeling/naive_bayes.py
  • data_science/SMSModel/reporting/__init__.py
  • data_science/SMSModel/reporting/dataset_split_report.py
  • data_science/SMSModel/reports/dataset_split_summary.json
  • data_science/SMSModel/reports/dataset_split_summary.md
  • data_science/SMSModel/reports/model_evaluation/naive_bayes_baseline/model_evaluation.json
  • data_science/SMSModel/reports/model_evaluation/naive_bayes_baseline/model_evaluation.md
  • data_science/SMSModel/run_naive_bayes_baseline.py
  • data_science/SMSModel/template_grouping/__init__.py
  • data_science/SMSModel/template_grouping/config.py
  • data_science/SMSModel/template_grouping/fingerprint.py
  • data_science/SMSModel/template_grouping/service.py
  • data_science/SMSModel/template_grouping/similarity.py
  • data_science/SMSModel/tokenization/__init__.py
  • data_science/SMSModel/tokenization/kiwi_tokenizer.py
  • data_science/SMSModel/train_sms.py
  • pytest.ini
  • requirements.txt
  • tests/analysis/text/test_naive_bayes_analyzer.py
  • tests/analysis/text/test_preprocessing.py
  • tests/data_science/SMSModel/__init__.py
  • tests/data_science/SMSModel/evaluation/__init__.py
  • tests/data_science/SMSModel/evaluation/conftest.py
  • tests/data_science/SMSModel/evaluation/test_base.py
  • tests/data_science/SMSModel/evaluation/test_evaluator.py
  • tests/data_science/SMSModel/evaluation/test_latency.py
  • tests/data_science/SMSModel/evaluation/test_metrics.py
  • tests/data_science/SMSModel/evaluation/test_reporting.py
  • tests/data_science/SMSModel/evaluation/test_threshold.py
  • tests/data_science/SMSModel/modeling/conftest.py
  • tests/data_science/SMSModel/modeling/test_artifacts.py
  • tests/data_science/SMSModel/modeling/test_baseline_runner.py
  • tests/data_science/SMSModel/modeling/test_naive_bayes.py
  • tests/data_science/SMSModel/test_dataset_split_report.py
  • tests/data_science/SMSModel/test_dataset_splitting.py
  • tests/data_science/SMSModel/test_template_grouping.py
  • tests/data_science/SMSModel/tokenization/__init__.py
  • tests/data_science/SMSModel/tokenization/test_kiwi_tokenizer.py
  • tests/data_science/__init__.py

Comment thread app/analysis/text/naive_bayes_analyzer.py Outdated
Comment thread app/analysis/text/naive_bayes_analyzer.py Outdated
Comment thread app/analysis/text/naive_bayes_analyzer.py
Comment on lines +14 to +20
MANIFEST_COLUMNS = [
"text_fingerprint",
"template_group_id",
"split",
"label",
"type",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use DatasetSplitConfig when creating manifests.

DatasetSplitConfig supports custom key column names, but MANIFEST_COLUMNS always requires the default names. A non-default configuration can create valid splits, then fail in save_split_manifest with missing default columns.

Pass DatasetSplitConfig to build_split_manifest and save_split_manifest. Derive fingerprint, group, and label manifest columns from that config. Add a round-trip test with renamed columns.

Also applies to: 24-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/SMSModel/dataset_splitting/manifest.py` around lines 14 - 20,
Update the manifest-building and saving flow to accept and use
DatasetSplitConfig, including build_split_manifest and save_split_manifest,
instead of relying on fixed MANIFEST_COLUMNS names. Derive the fingerprint,
group, and label columns from the configuration so renamed key columns work end
to end, and add a round-trip test covering custom column names.

Comment thread data_science/SMSModel/dataset_splitting/splitter.py Outdated
Comment on lines +28 to +37
def __post_init__(self) -> None:
values = np.asarray(self.values)
if values.ndim != 1:
raise ValueError("score values must be one-dimensional")
if not np.isfinite(values).all():
raise ValueError("score values must contain only finite numbers")
if self.score_type == ScoreType.PROBABILITY and (
(values < 0.0) | (values > 1.0)
).any():
raise ValueError("probability scores must be between 0 and 1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Store the normalized NumPy array in values.

Line 29 validates np.asarray(self.values) but leaves self.values unchanged. A list input passes validation and later exposes a list instead of the required np.ndarray.

Proposed fix
     def __post_init__(self) -> None:
-        values = np.asarray(self.values)
+        values = np.asarray(self.values, dtype=float)
         if values.ndim != 1:
             raise ValueError("score values must be one-dimensional")
         if not np.isfinite(values).all():
             raise ValueError("score values must contain only finite numbers")
+        object.__setattr__(self, "values", values)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __post_init__(self) -> None:
values = np.asarray(self.values)
if values.ndim != 1:
raise ValueError("score values must be one-dimensional")
if not np.isfinite(values).all():
raise ValueError("score values must contain only finite numbers")
if self.score_type == ScoreType.PROBABILITY and (
(values < 0.0) | (values > 1.0)
).any():
raise ValueError("probability scores must be between 0 and 1")
def __post_init__(self) -> None:
values = np.asarray(self.values, dtype=float)
if values.ndim != 1:
raise ValueError("score values must be one-dimensional")
if not np.isfinite(values).all():
raise ValueError("score values must contain only finite numbers")
object.__setattr__(self, "values", values)
if self.score_type == ScoreType.PROBABILITY and (
(values < 0.0) | (values > 1.0)
).any():
raise ValueError("probability scores must be between 0 and 1")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/SMSModel/modeling/base.py` around lines 28 - 37, Update
__post_init__ to assign the normalized result of np.asarray(self.values) back to
self.values before validation, so list inputs are stored as NumPy arrays.
Preserve the existing dimensionality, finiteness, and probability-range checks.

Comment thread data_science/SMSModel/modeling/naive_bayes.py
Comment thread data_science/SMSModel/README.md Outdated
Comment thread Dockerfile Outdated
Comment thread tests/data_science/SMSModel/test_template_grouping.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
app/analysis/router.py (1)

25-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silence Ruff B008 for FastAPI Depends.

Depends(get_analysis_service) is valid FastAPI dependencies syntax. Add a project-level Ruff ignore or extend-immutable-calls exemption for this rule so CI lints do not block this valid code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/analysis/router.py` at line 25, Update the project Ruff configuration to
exempt FastAPI dependency calls such as Depends(get_analysis_service) from B008,
using the appropriate ignore or extend-immutable-calls setting. Keep the
dependency declaration in the router unchanged and apply the exemption
project-wide.

Source: Linters/SAST tools

tests/analysis/test_service.py (1)

145-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a discard name for the unused test value.

Ruff RUF059 flags text_analysis in this unpacking. The test only checks naive_bayes_score and llm_available. Rename the binding to _text_analysis, or assert its contents.

♻️ Proposed fix
-        text_analysis,
+        _text_analysis,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/analysis/test_service.py` around lines 145 - 149, Update the unpacking
result from service._analyze_text_hybrid in the affected test to bind the unused
first value as _text_analysis, while preserving the existing assertions for
naive_bayes_score and llm_available.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/analysis/scoring.py`:
- Around line 152-154: Update the scoring flow around _combine_text_track_score
so BOTH_ENGINES_UNAVAILABLE_FALLBACK_SCORE remains an effective text-track
contribution even when text_available is false. Ensure weight normalization and
llm_contrib use that fallback instead of assigning all weight to the zero-score
rules track; alternatively return the pipeline error response. Update the
regression scenario in tests/analysis/test_service.py to require at least
MEDIUM.

In `@data_science/SMSModel/README.md`:
- Line 21: Change the `실행 방법` heading in the README from its current level to an
H2 heading, preserving the existing heading text and surrounding content.

In `@data_science/SMSModel/SMSDataModel.ipynb`:
- Around line 139-140: Update the evaluation feature construction around
ts._extract_struct_features so it uses the same inputs as train_sms.py and the
API inference pipeline. Remove df_eval["has_url"] from this call unless URL
values are also propagated consistently through training, holdout evaluation,
and inference; keep the resulting X construction aligned with the trained
feature matrix.

In `@data_science/SMSModel/train_sms.py`:
- Around line 400-414: Update the model-selection condition around best.update
so it explicitly tracks whether the current candidate and best candidate satisfy
TARGET_PHISHING_RECALL. Prefer a target-satisfying candidate according to the
existing recall_normal ranking, allow later fallback candidates to replace
earlier fallback candidates when rec_p is higher, and never let a fallback
candidate displace a target-satisfying best candidate.

In `@data_science/VoiceModel/train_voice.py`:
- Around line 526-541: Update the model-selection condition in the training loop
so candidates meeting TARGET_PHISHING_RECALL always take precedence over
fallback candidates, regardless of recall_normal. Only use the best["model"] is
None fallback when no target-recall candidate has been selected, while
preserving the existing metric updates in best.update.

In `@data_science/VoiceModel/VoiceDataModel.ipynb`:
- Around line 167-182: Rebuild and save matching ComplementNB model and
vectorizer artifacts, then rerun the notebook so the scoring cell produces valid
risk_score values. In the scoring flow before model.predict_proba(X), add a
feature-count compatibility check between X and the model’s expected input
features, raising a direct message instructing users to regenerate matching
artifacts when they differ.

---

Nitpick comments:
In `@app/analysis/router.py`:
- Line 25: Update the project Ruff configuration to exempt FastAPI dependency
calls such as Depends(get_analysis_service) from B008, using the appropriate
ignore or extend-immutable-calls setting. Keep the dependency declaration in the
router unchanged and apply the exemption project-wide.

In `@tests/analysis/test_service.py`:
- Around line 145-149: Update the unpacking result from
service._analyze_text_hybrid in the affected test to bind the unused first value
as _text_analysis, while preserving the existing assertions for
naive_bayes_score and llm_available.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ce19419-efd5-47e7-a37a-0f0b5a3df3e6

📥 Commits

Reviewing files that changed from the base of the PR and between 5c15ecc and bdf74a4.

📒 Files selected for processing (93)
  • Dockerfile
  • app/analysis/execution.py
  • app/analysis/ports.py
  • app/analysis/router.py
  • app/analysis/rules/analyzer.py
  • app/analysis/schemas.py
  • app/analysis/scoring.py
  • app/analysis/service.py
  • app/analysis/text/gemini_analyzer.py
  • app/analysis/text/naive_bayes_analyzer.py
  • app/analysis/text/preprocessing.py
  • app/analysis/url/analyzer.py
  • app/analysis/url/tracker.py
  • app/chat/schemas.py
  • app/chat/service.py
  • app/core/config.py
  • app/infrastructure/errors.py
  • app/infrastructure/gemini/client.py
  • app/infrastructure/google_safe_browsing/client.py
  • app/infrastructure/http_retry.py
  • app/infrastructure/mock_provider.py
  • app/infrastructure/rabbitmq/connection.py
  • app/infrastructure/rabbitmq/consumer.py
  • app/infrastructure/rabbitmq/dead_letter.py
  • app/infrastructure/rabbitmq/handler.py
  • app/infrastructure/rabbitmq/publisher.py
  • app/infrastructure/rabbitmq/result_factory.py
  • app/infrastructure/rabbitmq/schemas.py
  • app/infrastructure/virustotal/client.py
  • app/main.py
  • data_science/SMSModel/README.md
  • data_science/SMSModel/SMSDataModel.ipynb
  • data_science/SMSModel/dataset_splitting/__init__.py
  • data_science/SMSModel/dataset_splitting/manifest.py
  • data_science/SMSModel/dataset_splitting/splitter.py
  • data_science/SMSModel/dataset_splitting/validation.py
  • data_science/SMSModel/evaluation/__init__.py
  • data_science/SMSModel/evaluation/evaluator.py
  • data_science/SMSModel/evaluation/latency.py
  • data_science/SMSModel/evaluation/metrics.py
  • data_science/SMSModel/evaluation/reporting.py
  • data_science/SMSModel/evaluation/threshold.py
  • data_science/SMSModel/modeling/__init__.py
  • data_science/SMSModel/modeling/artifacts.py
  • data_science/SMSModel/modeling/base.py
  • data_science/SMSModel/modeling/naive_bayes.py
  • data_science/SMSModel/reporting/__init__.py
  • data_science/SMSModel/reporting/dataset_split_report.py
  • data_science/SMSModel/run_naive_bayes_baseline.py
  • data_science/SMSModel/template_grouping/config.py
  • data_science/SMSModel/template_grouping/fingerprint.py
  • data_science/SMSModel/template_grouping/similarity.py
  • data_science/SMSModel/tokenization/__init__.py
  • data_science/SMSModel/tokenization/kiwi_tokenizer.py
  • data_science/SMSModel/train_sms.py
  • data_science/VoiceModel/VoiceDataModel.ipynb
  • data_science/VoiceModel/train_voice.py
  • tests/analysis/rules/test_analyzer.py
  • tests/analysis/test_execution.py
  • tests/analysis/test_router.py
  • tests/analysis/test_scoring.py
  • tests/analysis/test_service.py
  • tests/analysis/text/test_gemini_analyzer.py
  • tests/analysis/text/test_naive_bayes_analyzer.py
  • tests/analysis/text/test_nb_masking_accuracy.py
  • tests/analysis/text/test_preprocessing.py
  • tests/analysis/url/test_analyzer.py
  • tests/analysis/url/test_extractor.py
  • tests/analysis/url/test_tracker.py
  • tests/chat/test_router.py
  • tests/chat/test_service.py
  • tests/data_science/SMSModel/evaluation/conftest.py
  • tests/data_science/SMSModel/evaluation/test_threshold.py
  • tests/data_science/SMSModel/modeling/test_artifacts.py
  • tests/data_science/SMSModel/modeling/test_baseline_runner.py
  • tests/data_science/SMSModel/modeling/test_naive_bayes.py
  • tests/data_science/SMSModel/test_dataset_split_report.py
  • tests/data_science/SMSModel/test_dataset_splitting.py
  • tests/data_science/SMSModel/test_template_grouping.py
  • tests/data_science/SMSModel/tokenization/__init__.py
  • tests/data_science/SMSModel/tokenization/test_kiwi_tokenizer.py
  • tests/infrastructure/rabbitmq/test_connection.py
  • tests/infrastructure/rabbitmq/test_consumer.py
  • tests/infrastructure/rabbitmq/test_dead_letter.py
  • tests/infrastructure/rabbitmq/test_handler.py
  • tests/infrastructure/rabbitmq/test_lifecycle.py
  • tests/infrastructure/rabbitmq/test_publisher.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
  • tests/infrastructure/rabbitmq/test_result_schemas.py
  • tests/infrastructure/rabbitmq/test_schemas.py
  • tests/infrastructure/test_http_retry.py
  • tests/infrastructure/test_virustotal_client.py
  • tests/integration/test_url_tracker.py
💤 Files with no reviewable changes (2)
  • data_science/SMSModel/template_grouping/config.py
  • tests/data_science/SMSModel/evaluation/conftest.py
🚧 Files skipped from review as they are similar to previous changes (28)
  • tests/data_science/SMSModel/evaluation/test_threshold.py
  • data_science/SMSModel/modeling/init.py
  • data_science/SMSModel/reporting/init.py
  • Dockerfile
  • tests/data_science/SMSModel/tokenization/init.py
  • tests/analysis/text/test_preprocessing.py
  • data_science/SMSModel/dataset_splitting/init.py
  • data_science/SMSModel/tokenization/init.py
  • data_science/SMSModel/dataset_splitting/validation.py
  • tests/data_science/SMSModel/modeling/test_naive_bayes.py
  • tests/data_science/SMSModel/modeling/test_baseline_runner.py
  • data_science/SMSModel/evaluation/metrics.py
  • data_science/SMSModel/evaluation/init.py
  • data_science/SMSModel/template_grouping/fingerprint.py
  • tests/data_science/SMSModel/test_dataset_split_report.py
  • data_science/SMSModel/dataset_splitting/splitter.py
  • data_science/SMSModel/run_naive_bayes_baseline.py
  • tests/analysis/text/test_naive_bayes_analyzer.py
  • app/core/config.py
  • data_science/SMSModel/modeling/base.py
  • data_science/SMSModel/evaluation/evaluator.py
  • data_science/SMSModel/evaluation/threshold.py
  • tests/data_science/SMSModel/test_template_grouping.py
  • data_science/SMSModel/tokenization/kiwi_tokenizer.py
  • data_science/SMSModel/template_grouping/similarity.py
  • app/analysis/text/preprocessing.py
  • data_science/SMSModel/evaluation/latency.py
  • data_science/SMSModel/evaluation/reporting.py

Comment thread app/analysis/scoring.py
Comment thread data_science/SMSModel/README.md Outdated
Comment thread data_science/SMSModel/SMSDataModel.ipynb Outdated
Comment thread data_science/SMSModel/train_sms.py Outdated
Comment on lines +526 to +541
if (
rec_p >= TARGET_PHISHING_RECALL
and rec_n > best["recall_normal"]
or best["model"] is None
and rec_p > best["recall_phishing"]
):
best.update(
{
"model": model,
"model_name": model_cls.__name__,
"alpha": alpha,
"threshold": threshold,
"recall_phishing": rec_p,
"recall_normal": rec_n,
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prioritize target-recall candidates over fallback candidates.

The current condition can retain a model below TARGET_PHISHING_RECALL when it has higher recall_normal than a later target-meeting model. This violates the documented selection order.

Proposed fix
-                if (
-                    rec_p >= TARGET_PHISHING_RECALL
-                    and rec_n > best["recall_normal"]
-                    or best["model"] is None
-                    and rec_p > best["recall_phishing"]
-                ):
+                candidate_meets_target = rec_p >= TARGET_PHISHING_RECALL
+                best_meets_target = (
+                    best["model"] is not None
+                    and best["recall_phishing"] >= TARGET_PHISHING_RECALL
+                )
+                if (
+                    best["model"] is None
+                    or (
+                        candidate_meets_target
+                        and (
+                            not best_meets_target
+                            or rec_n > best["recall_normal"]
+                        )
+                    )
+                    or (
+                        not candidate_meets_target
+                        and not best_meets_target
+                        and rec_p > best["recall_phishing"]
+                    )
+                ):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
rec_p >= TARGET_PHISHING_RECALL
and rec_n > best["recall_normal"]
or best["model"] is None
and rec_p > best["recall_phishing"]
):
best.update(
{
"model": model,
"model_name": model_cls.__name__,
"alpha": alpha,
"threshold": threshold,
"recall_phishing": rec_p,
"recall_normal": rec_n,
}
)
candidate_meets_target = rec_p >= TARGET_PHISHING_RECALL
best_meets_target = (
best["model"] is not None
and best["recall_phishing"] >= TARGET_PHISHING_RECALL
)
if (
best["model"] is None
or (
candidate_meets_target
and (
not best_meets_target
or rec_n > best["recall_normal"]
)
)
or (
not candidate_meets_target
and not best_meets_target
and rec_p > best["recall_phishing"]
)
):
best.update(
{
"model": model,
"model_name": model_cls.__name__,
"alpha": alpha,
"threshold": threshold,
"recall_phishing": rec_p,
"recall_normal": rec_n,
}
)
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 527-528: Parenthesize a and b expressions when chaining and and or together, to make the precedence clear

Parenthesize the and subexpression

(RUF021)


[warning] 529-530: Parenthesize a and b expressions when chaining and and or together, to make the precedence clear

Parenthesize the and subexpression

(RUF021)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/VoiceModel/train_voice.py` around lines 526 - 541, Update the
model-selection condition in the training loop so candidates meeting
TARGET_PHISHING_RECALL always take precedence over fallback candidates,
regardless of recall_normal. Only use the best["model"] is None fallback when no
target-recall candidate has been selected, while preserving the existing metric
updates in best.update.

Comment on lines +167 to +182
"raw_scores = (prob_phishing * 100).astype(int)\n",
"risk_scores = np.array(\n",
" [\n",
" tv._apply_risk_floor(s, struct[i], df_eval[\"text_clean\"].iloc[i])\n",
" for i, s in enumerate(raw_scores)\n",
" ]\n",
")\n",
"\n",
"df_eval[\"prob_phishing\"] = prob_phishing\n",
"df_eval[\"risk_score\"] = risk_scores\n",
"df_eval[\"risk_level\"] = df_eval[\"risk_score\"].apply(tv._map_risk_level)\n",
"df_eval[\"risk_score\"] = risk_scores\n",
"df_eval[\"risk_level\"] = df_eval[\"risk_score\"].apply(tv._map_risk_level)\n",
"\n",
"print(f\"[Score] test+holdout {len(df_eval)}건 채점 완료 | \"\n",
" f\"label={df_eval['label'].value_counts().to_dict()}\")\n"
"print(\n",
" f\"[Score] test+holdout {len(df_eval)}건 채점 완료 | \"\n",
" f\"label={df_eval['label'].value_counts().to_dict()}\"\n",
")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore a runnable artifact and feature schema.

This scoring cell has saved output that shows X has 8010 features, but ComplementNB is expecting 8008 features as input. The cell cannot produce risk_score values.

Rebuild matching model and vectorizer artifacts, then rerun the notebook. Add a feature-schema compatibility check before model.predict_proba(X) so stale artifacts fail with a direct remediation message.

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 174-174: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data_science/VoiceModel/VoiceDataModel.ipynb` around lines 167 - 182, Rebuild
and save matching ComplementNB model and vectorizer artifacts, then rerun the
notebook so the scoring cell produces valid risk_score values. In the scoring
flow before model.predict_proba(X), add a feature-count compatibility check
between X and the model’s expected input features, raising a direct message
instructing users to regenerate matching artifacts when they differ.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature or functional additions to the application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant