Skip to content

Test(#37): Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 (3/3) - #62

Merged
pearseona merged 9 commits into
developfrom
test/37-hybrid-evaluation
Aug 13, 2026
Merged

Test(#37): Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 (3/3)#62
pearseona merged 9 commits into
developfrom
test/37-hybrid-evaluation

Conversation

@pearseona

@pearseona pearseona commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

📝 개요

고정된 test split을 기준으로 Stacking-Only, Claude-only, Hybrid 분석 방식을 동일한 조건에서 비교 평가했습니다.

Validation split에서 선정한 하이브리드 임계값을 고정해 test 데이터 재조정을 방지했으며, 정확도뿐 아니라 LLM 호출률, 지연시간, 토큰 사용량 및 비용을 함께 측정했습니다.

평가 결과 Hybrid 방식은 Recall 1.0을 유지하면서 Stacking-only 대비 F2를 개선했고, Calude-only 대비 LLM 호출률과 평균 비용 및 지연시간을 절감했습니다.

🔗 관련 이슈

🎯 주요 변경 사항

  • Stacking-only, Claude-only, Hybrid 3가지 모드의 평가 파이프라인을 구현했습니다.
  • Validation split에서 선정한 임계값을 hybrid_policy.json에 저장하고 test 평가 시 고정해서 사용하도록 구성했습니다.
  • 고정된 test split 123건을 대상으로 다음 지표를 산출했습니다.
    • Accuracy, Precision, Recall, F1, F2
    • 평균, P50, P95 지연시간
    • LLM 호출률 및 입력·출력 토큰 수
    • 메시지당 예상 비용
    • fallback, 전체 엔진 실패 및 결과 없음 건수
  • Bedrock Claude 예측 캐시를 추가해 성공한 요청은 재호출하지 않고, 누락되거나 실패한 항목만 다시 수집하도록 구현했습니다.
  • 기본 평가 명령은 외부 Bedrock 호출이 발생하지 않는 offline 모드로 동작하도록 구성했습니다.
  • JSON, CSV, Markdown 형식의 재현 가능한 비교 보고서를 생성했습니다.
  • 평가 레코드에는 메시지 원문, 개인정보 및 AWS 자격 증명이 저장되지 않도록 했습니다.
  • LLM 실패 시 Stacking 결과로 fallback되는 경로와 전체 분석 엔진 실패 경로를 테스트했습니다.
  • 기존 백엔드 연동을 위해 gemini_* 응답 필드의 하위 호환 alias를 유지했습니다.
  • 평가 실행 및 보고서 재생성 방법을 data_science/SMSModel/README.md에 문서화했습니다.

재현 방법

# 저장된 캐시를 이용한 오프라인 평가
& .\.venv\Scripts\python.exe -m `
  data_science.SMSModel.run_hybrid_evaluation --offline

# 비교 보고서 생성
& .\.venv\Scripts\python.exe -m `
  data_science.SMSModel.generate_hybrid_evaluation_report `
  --input-price-per-million 1.0 `
  --output-price-per-million 5.0 `
  --currency USD `
  --pricing-as-of 2026-08-13

✅ PR 체크리스트

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

Summary by CodeRabbit

  • New Features

    • Added hybrid SMS evaluation across self-model, LLM-only, and hybrid modes.
    • Added reproducible prediction caching with safe metadata handling and offline evaluation.
    • Added comparison reports in JSON, Markdown, and CSV formats, including accuracy, latency, availability, usage, and cost metrics.
    • Added LLM usage, latency, cache, fallback, and availability details to analysis results.
  • Documentation

    • Added instructions for offline evaluation, selective cloud prediction collection, pricing, and privacy safeguards.
  • Bug Fixes

    • Improved failure handling with safe fallback and unavailable-result reporting.

@pearseona pearseona self-assigned this Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 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: 64 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: 60f0c0db-106c-43d1-a858-fa84ab4274bb

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc56fe and 2db3a1a.

📒 Files selected for processing (14)
  • .gitignore
  • app/infrastructure/rabbitmq/result_factory.py
  • app/infrastructure/rabbitmq/schemas.py
  • data_science/SMSModel/README.md
  • data_science/SMSModel/artifacts/stacking/hybrid_policy.json
  • data_science/SMSModel/hybrid_evaluation/__init__.py
  • data_science/SMSModel/hybrid_evaluation/cache.py
  • data_science/SMSModel/hybrid_evaluation/metrics.py
  • data_science/SMSModel/hybrid_evaluation/reporting.py
  • data_science/SMSModel/hybrid_evaluation/runner.py
  • data_science/SMSModel/reports/hybrid_evaluation/comparison_report.json
  • data_science/SMSModel/reports/hybrid_evaluation/comparison_report.md
  • data_science/SMSModel/run_hybrid_evaluation.py
  • tests/analysis/text/test_hybrid_llm_failure_integration.py
📝 Walkthrough

Walkthrough

The PR adds a hybrid SMS evaluation package with metrics, caching, three-mode execution, offline Bedrock workflows, report generation, validation, artifacts, documentation, and regression tests.

Changes

Hybrid evaluation workflow

Layer / File(s) Summary
Evaluation contracts and metrics
data_science/SMSModel/hybrid_evaluation/models.py, metrics.py, __init__.py, tests/.../test_metrics.py
Adds immutable evaluation records, classification, latency, operational, token, and cost metrics with validation and serialization.
Prediction cache and safe persistence
data_science/SMSModel/hybrid_evaluation/cache.py, tests/.../test_cache.py, .gitignore
Adds fingerprinted Claude prediction caching, atomic persistence, schema checks, completeness validation, sanitized errors, and cache-response reconstruction.
Mode execution and analyzer telemetry
data_science/SMSModel/hybrid_evaluation/runner.py, app/analysis/text/hybrid_analyzer.py, tests/analysis/*, tests/.../test_runner.py
Adds ordered self-model, LLM-only, and hybrid evaluation. Records routing, fallback, availability, provider, model, usage, latency, cache, and error metadata.
Fixed-split collection and offline evaluation
data_science/SMSModel/run_hybrid_evaluation.py, artifacts/stacking/*, README.md
Adds fixed test-split validation, selective Claude collection, cache-backed offline execution, atomic record storage, CLI modes, and reproduction instructions.
Comparison reports and generated artifacts
data_science/SMSModel/hybrid_evaluation/reporting.py, generate_hybrid_evaluation_report.py, reports/hybrid_evaluation/*, tests/.../test_reporting.py
Adds validated JSON, Markdown, and CSV comparison reports with metrics, pricing, provenance, target assessments, and privacy-preserving output checks.

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

Mergeability Score: 🟡 Moderate · up to 1fc56

The PR adds hybrid evaluation and fallback behavior, but current results can misclassify stacking-derived outcomes and malformed latency values can break later offline evaluations; the documented fresh-clone workflow also lacks the required cache, and some telemetry is lost in published results. These concrete correctness and operational issues should be fixed or explicitly accepted before merge.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ClaudeTestCache
  participant HybridEvaluationRunner
  participant HybridTextAnalyzer
  participant ReportGenerator
  CLI->>ClaudeTestCache: Load and update fixed-split predictions
  CLI->>HybridEvaluationRunner: Run three evaluation modes
  HybridEvaluationRunner->>HybridTextAnalyzer: Execute model and routing paths
  HybridEvaluationRunner-->>CLI: Return evaluation records
  CLI->>ReportGenerator: Build comparison outputs
  ReportGenerator-->>CLI: Write JSON, Markdown, and CSV reports
Loading

Possibly related PRs

  • SafeFam/SafeFam_AI#59: Adds the hybrid Stacking/Gemini analyzer and routing policy extended by this evaluation workflow.
  • SafeFam/SafeFam_AI#61: Provides the provider-neutral LLM analyzer and hybrid policy interfaces used here.
  • SafeFam/SafeFam_AI#52: Provides the stacking classifier and artifacts used by hybrid evaluation.

Suggested labels: feat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.06% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 Bedrock Claude 하이브리드 성능·비용 평가와 fallback 검증이라는 변경의 주요 내용을 정확히 요약합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/37-hybrid-evaluation

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.

@pearseona pearseona changed the title Test: Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 Test(#37): Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 Aug 13, 2026

@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: 9

🧹 Nitpick comments (12)
tests/data_science/SMSModel/hybrid_evaluation/test_cache.py (2)

149-168: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for the "unexpected fingerprints" completeness branch.

require_complete in data_science/SMSModel/hybrid_evaluation/cache.py (lines 339-359) raises for two conditions: missing predictions and predictions outside the current test split. Only the missing branch is tested. The second branch protects the frozen test split from contamination by validation-split predictions, so it deserves a test.

💚 Proposed additional test
+def test_require_complete_rejects_predictions_outside_split(
+    tmp_path,
+) -> None:
+    cache = build_cache(tmp_path)
+
+    cache.store_analysis(
+        text_fingerprint=FINGERPRINT_A,
+        analysis=successful_analysis(),
+    )
+    cache.store_analysis(
+        text_fingerprint=FINGERPRINT_B,
+        analysis=successful_analysis(),
+    )
+
+    with pytest.raises(
+        RuntimeError,
+        match="outside",
+    ):
+        cache.require_complete({FINGERPRINT_A})
+
+
🤖 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/data_science/SMSModel/hybrid_evaluation/test_cache.py` around lines 149
- 168, Add a test alongside test_require_complete_rejects_missing_predictions
that stores a prediction for a fingerprint outside the requested set, then calls
require_complete with the expected fingerprints and asserts it raises
RuntimeError for the unexpected-fingerprint condition. Use existing cache
builders and analysis fixtures, and verify the error identifies the completeness
failure.

106-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider parametrizing region in the metadata mismatch test.

build_cache passes region="us-east-1", but the mismatch test does not cover a changed region. If the cache persists and validates region, add it to the parametrization. If region is not validated, confirm that this is intended, because a cache collected in another region can be reused silently.

🤖 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/data_science/SMSModel/hybrid_evaluation/test_cache.py` around lines 106
- 146, The test_rejects_incompatible_cache parametrization should include a
changed region case, using a value different from build_cache’s configured
region, so load() must raise the existing metadata mismatch error. If cache
metadata validation omits region, update that validation to reject mismatched
regions before relying on the expanded test.
data_science/SMSModel/README.md (1)

39-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a POSIX command form.

The new examples use PowerShell syntax and a Windows virtual environment path. The earlier section of this README uses python -m data_science.SMSModel.train_sms. Add the equivalent python -m ... invocation so that macOS and Linux contributors can reproduce the evaluation.

🤖 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/README.md` around lines 39 - 59, Add POSIX-compatible
evaluation commands to the README alongside the PowerShell examples, using the
existing `python -m data_science.SMSModel...` invocation style and a Unix
virtual-environment path. Include both offline evaluation/report generation and
the `--collect` invocation so macOS and Linux users can reproduce the documented
workflows.
data_science/SMSModel/hybrid_evaluation/metrics.py (1)

186-242: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider validating outcome element types.

calculate_cost_metrics raises TypeError for non-TokenUsage items. calculate_operational_metrics does not apply the same check. A plain dict or a mock object passes silently and produces truthiness-based counts. Add the same guard for symmetry.

♻️ Proposed guard
     if len(outcomes) == 0:
         raise ValueError(
             "cannot calculate operational metrics "
             "from empty outcomes"
         )
 
+    for outcome in outcomes:
+        if not isinstance(outcome, OperationalOutcome):
+            raise TypeError(
+                "outcomes must contain OperationalOutcome instances"
+            )
+
🤖 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/hybrid_evaluation/metrics.py` around lines 186 - 242,
Update calculate_operational_metrics to validate every outcomes element is an
OperationalOutcome before calculating counts, raising TypeError for invalid
values like dicts or mocks, matching the existing guard behavior in
calculate_cost_metrics.
data_science/SMSModel/artifacts/stacking/hybrid_policy.json (1)

5-16: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Identical hybrid policy metrics are stored in two artifacts. Both files now carry the same f2, precision, recall, llm_call_count, llm_call_rate, normal_probability_max, phishing_probability_min, target_recall, target_recall_met, and validation_count values. If one file is regenerated without the other, the two artifacts disagree and readers cannot tell which one the evaluation applied.

  • data_science/SMSModel/artifacts/stacking/hybrid_policy.json#L5-L16: keep this file as the authoritative frozen policy, since the runner loads it.
  • data_science/SMSModel/artifacts/stacking/metadata.json#L4-L15: replace the duplicated metric values with a reference to hybrid_policy.json and its schema_version, or confirm that one generator writes both files in a single step.
🤖 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/artifacts/stacking/hybrid_policy.json` around lines 5 -
16, The hybrid policy metrics are duplicated across two artifacts, allowing them
to diverge. Keep data_science/SMSModel/artifacts/stacking/hybrid_policy.json
lines 5-16 as the authoritative frozen policy loaded by the runner; update
data_science/SMSModel/artifacts/stacking/metadata.json lines 4-15 to reference
that policy and its schema_version, or ensure the generator writes both
artifacts atomically in one step.
tests/data_science/SMSModel/hybrid_evaluation/test_runner.py (1)

314-323: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for a non-string text.

EvaluationSample.__post_init__ raises TypeError for a non-string text. The parametrized cases only cover ValueError. One extra case locks in both contracts.

💚 Proposed addition
 def test_rejects_invalid_samples(sample: tuple[str, str, str]) -> None:
     with pytest.raises(ValueError):
         EvaluationSample(*sample)
+
+
+def test_rejects_non_string_text() -> None:
+    with pytest.raises(TypeError):
+        EvaluationSample("id", None, "normal")
🤖 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/data_science/SMSModel/hybrid_evaluation/test_runner.py` around lines
314 - 323, Add a parametrized sample with a non-string text value to
test_rejects_invalid_samples and assert that constructing EvaluationSample
raises TypeError for that case, while preserving the existing ValueError
assertions for invalid string samples.
data_science/SMSModel/run_hybrid_evaluation.py (1)

94-144: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

load_test_data triggers split report regeneration.

split_data writes the dataset split JSON and Markdown reports, and prints split summaries. Evaluation therefore rewrites training artifacts as a side effect. This can create unrelated diffs in the repository during an evaluation run.

Consider loading the manifest directly, or document the side effect in data_science/SMSModel/README.md.

🤖 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/run_hybrid_evaluation.py` around lines 94 - 144, Update
load_test_data to load and reconstruct the test split directly from
SPLIT_MANIFEST_PATH instead of calling split_data, avoiding regeneration of
split reports and other training artifacts during evaluation. Preserve the
existing required-column, non-empty, unique-fingerprint, and supported-label
validations.
tests/analysis/text/test_stacking_analyzer.py (1)

354-381: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test duplicates the existing inference-failure test.

test_inference_error_returns_fail_safe_result at Line 310 uses the same monkeypatching and only differs in the raised exception type. Parametrize the exception type on one test instead of keeping two near-identical bodies.

🤖 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/text/test_stacking_analyzer.py` around lines 354 - 381,
Consolidate the duplicate stacking failure tests by parameterizing
test_inference_error_returns_fail_safe_result with the relevant exception types,
including ValueError. Remove
test_feature_extraction_failure_makes_stacking_unavailable and keep the shared
fail-safe assertions in the single parameterized test.
data_science/SMSModel/hybrid_evaluation/cache.py (2)

136-140: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

entries deep-copies the whole cache on every access.

The collection loop in data_science/SMSModel/run_hybrid_evaluation.py Lines 286-288 reads cache.entries[fingerprint]["available"] once per sample. Each read copies all stored entries, so collection is quadratic in split size. The current split is small, so this is not urgent. A small accessor avoids the copy.

♻️ Proposed accessor
     `@property`
     def entries(self) -> dict[str, dict[str, Any]]:
         """외부 변경을 막기 위해 복사본 반환"""
 
         return deepcopy(self._entries)
+
+    def is_available(self, text_fingerprint: str) -> bool:
+        """단일 항목의 available 값만 조회"""
+
+        entry = self._entries.get(text_fingerprint)
+
+        return bool(entry and entry.get("available") is True)
🤖 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/hybrid_evaluation/cache.py` around lines 136 - 140,
Update the cache access pattern around the entries property in the cache class
to provide a non-copying accessor for a single fingerprint, then use that
accessor in the collection loop of run_hybrid_evaluation instead of repeatedly
reading cache.entries. Preserve entries’ defensive deep-copy behavior for
callers that need the full cache.

89-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shared helpers are copied across three modules. _atomic_write_json and _optional_token_count each exist in more than one file. The copies can drift, which is how the loader and the writer already disagree about non-finite latency_ms. Place one implementation in a shared module inside the hybrid_evaluation package.

  • data_science/SMSModel/hybrid_evaluation/cache.py#L89-L115: move _atomic_write_json and _optional_token_count into a new shared module, for example hybrid_evaluation/io.py, and import them here.
  • data_science/SMSModel/hybrid_evaluation/runner.py#L228-L231: delete the local _optional_token_count and import the shared one.
  • data_science/SMSModel/run_hybrid_evaluation.py#L68-L92: delete the local _atomic_write_json and import the shared one.
🤖 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/hybrid_evaluation/cache.py` around lines 89 - 115,
Consolidate the duplicated _atomic_write_json and _optional_token_count helpers
into a shared hybrid_evaluation/io.py module, preserving their existing
behavior. In data_science/SMSModel/hybrid_evaluation/cache.py lines 89-115, move
both helpers and import them from the shared module; in
data_science/SMSModel/hybrid_evaluation/runner.py lines 228-231, delete the
local _optional_token_count and import the shared helper; in
data_science/SMSModel/run_hybrid_evaluation.py lines 68-92, delete the local
_atomic_write_json and import the shared helper.
data_science/SMSModel/hybrid_evaluation/runner.py (1)

99-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Latency for cached runs mixes measured and recorded values.

The measured wall time covers only the cache read. Adding the recorded provider latency produces a hybrid number that is neither pure local time nor pure provider time. The sum stays comparable across modes, but a reader of the report cannot separate the two parts.

Consider storing the recorded provider latency in a separate field, and document the composition in the report.

🤖 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/hybrid_evaluation/runner.py` around lines 99 - 104,
Separate the cache-read wall time from the recorded provider latency in the
evaluation result around the latency calculation in runner.py: preserve the
measured local latency, store cached_llm provider latency in its own field, and
update the report output to label and document how the two values compose for
cached runs.
tests/analysis/text/test_hybrid_llm_failure_integration.py (1)

121-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

"secret" not in caplog.text is fragile.

The assertion fails for any unrelated log text that contains the substring, for example a module path or the word "secrets". Assert on the full injected token instead.

🤖 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/text/test_hybrid_llm_failure_integration.py` around lines 121
- 124, Update the assertions in the hybrid LLM failure integration test to
remove the broad `"secret" not in caplog.text` substring check and instead
assert that the complete injected secret token is absent from the logs. Preserve
the existing checks for the other sensitive values.
🤖 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 @.gitignore:
- Around line 21-23: Document the offline evaluation prerequisite near the
ignored LLM evaluation artifacts: a fresh clone must first run with --collect
and AWS access to generate llm_test_predictions.json, unless the cache is
committed outside the ignored path. Ensure the documentation makes clear that
--offline cannot be used successfully without an existing cache.

In `@app/analysis/text/hybrid_analyzer.py`:
- Around line 143-148: Add llm_usage, llm_latency_ms, and llm_from_cache to the
published TextAnalysisDetail contract, then update result_factory.py to
explicitly map these fields from the analyzer output into the RabbitMQ payload.
Preserve the direct /analyze response and HybridEvaluationRunner behavior.

In `@data_science/SMSModel/artifacts/stacking/hybrid_policy.json`:
- Around line 1-19: Add model provenance fields to the policy artifact
represented by the stacking policy JSON, including a stable model identifier or
artifact fingerprint and the model threshold used to derive the probability
bands. Update the policy loader’s validation to compare these values with the
current model and reject mismatches instead of accepting stale thresholds;
preserve the existing split metadata and threshold-selection fields.

In `@data_science/SMSModel/hybrid_evaluation/__init__.py`:
- Around line 35-60: Sort the `__all__` entries in
`data_science/SMSModel/hybrid_evaluation/__init__.py` alphabetically to satisfy
Ruff RUF022, preserving every currently exported symbol and changing only their
order.

In `@data_science/SMSModel/hybrid_evaluation/cache.py`:
- Around line 311-320: Update the latency_ms validation in store_analysis to
reject non-finite numeric values, matching the finiteness rule used by
_validate_cache_entry, while preserving boolean and negative-value rejection.
Optionally configure _atomic_write_json with allow_nan=False so invalid JSON
cannot be written.

In `@data_science/SMSModel/hybrid_evaluation/reporting.py`:
- Around line 39-40: Update pricing_as_of validation in the reporting
input-validation flow to parse an ISO date and reject values later than August
12, 2026; retain the existing non-empty validation for invalid or missing
values. Update affected fixtures and generated reports to use an effective date
on or before August 12, 2026, without labeling future dates as current pricing.

In `@data_science/SMSModel/hybrid_evaluation/runner.py`:
- Around line 185-194: Align stacking-derived label computation in _run_hybrid
with _stacking_to_public_result and the classifier threshold: use
is_suspected_phishing, or consistently apply the selected stacking threshold,
before calculating hybrid metrics. Ensure probabilities around 0.09–0.39 and
STACKING_FALLBACK produce the same phishing/normal classification as self-model
mode.

In `@data_science/SMSModel/run_hybrid_evaluation.py`:
- Around line 406-448: Update async_main and its caller main so offline
explicitly controls execution: preserve the --collect/--offline validation,
prevent collect_missing_predictions from running when offline is true, and
ensure offline mode uses only cached predictions without Bedrock calls. Remove
the derived offline = arguments.offline or not arguments.collect behavior that
makes the guard unreachable, and correct the cache.py comment to state that
offline mode avoids provider calls rather than importing the LLM module.

In `@tests/analysis/text/test_hybrid_llm_failure_integration.py`:
- Around line 108-124: Update
test_failure_logs_do_not_expose_message_or_credentials to capture logs from
DEBUG level by changing the caplog.at_level setting, so its existing privacy
assertions inspect INFO, WARNING, and ERROR records.

---

Nitpick comments:
In `@data_science/SMSModel/artifacts/stacking/hybrid_policy.json`:
- Around line 5-16: The hybrid policy metrics are duplicated across two
artifacts, allowing them to diverge. Keep
data_science/SMSModel/artifacts/stacking/hybrid_policy.json lines 5-16 as the
authoritative frozen policy loaded by the runner; update
data_science/SMSModel/artifacts/stacking/metadata.json lines 4-15 to reference
that policy and its schema_version, or ensure the generator writes both
artifacts atomically in one step.

In `@data_science/SMSModel/hybrid_evaluation/cache.py`:
- Around line 136-140: Update the cache access pattern around the entries
property in the cache class to provide a non-copying accessor for a single
fingerprint, then use that accessor in the collection loop of
run_hybrid_evaluation instead of repeatedly reading cache.entries. Preserve
entries’ defensive deep-copy behavior for callers that need the full cache.
- Around line 89-115: Consolidate the duplicated _atomic_write_json and
_optional_token_count helpers into a shared hybrid_evaluation/io.py module,
preserving their existing behavior. In
data_science/SMSModel/hybrid_evaluation/cache.py lines 89-115, move both helpers
and import them from the shared module; in
data_science/SMSModel/hybrid_evaluation/runner.py lines 228-231, delete the
local _optional_token_count and import the shared helper; in
data_science/SMSModel/run_hybrid_evaluation.py lines 68-92, delete the local
_atomic_write_json and import the shared helper.

In `@data_science/SMSModel/hybrid_evaluation/metrics.py`:
- Around line 186-242: Update calculate_operational_metrics to validate every
outcomes element is an OperationalOutcome before calculating counts, raising
TypeError for invalid values like dicts or mocks, matching the existing guard
behavior in calculate_cost_metrics.

In `@data_science/SMSModel/hybrid_evaluation/runner.py`:
- Around line 99-104: Separate the cache-read wall time from the recorded
provider latency in the evaluation result around the latency calculation in
runner.py: preserve the measured local latency, store cached_llm provider
latency in its own field, and update the report output to label and document how
the two values compose for cached runs.

In `@data_science/SMSModel/README.md`:
- Around line 39-59: Add POSIX-compatible evaluation commands to the README
alongside the PowerShell examples, using the existing `python -m
data_science.SMSModel...` invocation style and a Unix virtual-environment path.
Include both offline evaluation/report generation and the `--collect` invocation
so macOS and Linux users can reproduce the documented workflows.

In `@data_science/SMSModel/run_hybrid_evaluation.py`:
- Around line 94-144: Update load_test_data to load and reconstruct the test
split directly from SPLIT_MANIFEST_PATH instead of calling split_data, avoiding
regeneration of split reports and other training artifacts during evaluation.
Preserve the existing required-column, non-empty, unique-fingerprint, and
supported-label validations.

In `@tests/analysis/text/test_hybrid_llm_failure_integration.py`:
- Around line 121-124: Update the assertions in the hybrid LLM failure
integration test to remove the broad `"secret" not in caplog.text` substring
check and instead assert that the complete injected secret token is absent from
the logs. Preserve the existing checks for the other sensitive values.

In `@tests/analysis/text/test_stacking_analyzer.py`:
- Around line 354-381: Consolidate the duplicate stacking failure tests by
parameterizing test_inference_error_returns_fail_safe_result with the relevant
exception types, including ValueError. Remove
test_feature_extraction_failure_makes_stacking_unavailable and keep the shared
fail-safe assertions in the single parameterized test.

In `@tests/data_science/SMSModel/hybrid_evaluation/test_cache.py`:
- Around line 149-168: Add a test alongside
test_require_complete_rejects_missing_predictions that stores a prediction for a
fingerprint outside the requested set, then calls require_complete with the
expected fingerprints and asserts it raises RuntimeError for the
unexpected-fingerprint condition. Use existing cache builders and analysis
fixtures, and verify the error identifies the completeness failure.
- Around line 106-146: The test_rejects_incompatible_cache parametrization
should include a changed region case, using a value different from build_cache’s
configured region, so load() must raise the existing metadata mismatch error. If
cache metadata validation omits region, update that validation to reject
mismatched regions before relying on the expanded test.

In `@tests/data_science/SMSModel/hybrid_evaluation/test_runner.py`:
- Around line 314-323: Add a parametrized sample with a non-string text value to
test_rejects_invalid_samples and assert that constructing EvaluationSample
raises TypeError for that case, while preserving the existing ValueError
assertions for invalid string samples.
🪄 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: c1063e8b-5f63-4adb-ae8c-ec69726037c9

📥 Commits

Reviewing files that changed from the base of the PR and between 78d56c4 and 1fc56fe.

⛔ Files ignored due to path filters (1)
  • data_science/SMSModel/reports/hybrid_evaluation/comparison_report.csv is excluded by !**/*.csv
📒 Files selected for processing (24)
  • .gitignore
  • app/analysis/text/hybrid_analyzer.py
  • data_science/SMSModel/README.md
  • data_science/SMSModel/artifacts/stacking/hybrid_policy.json
  • data_science/SMSModel/artifacts/stacking/metadata.json
  • data_science/SMSModel/generate_hybrid_evaluation_report.py
  • data_science/SMSModel/hybrid_evaluation/__init__.py
  • data_science/SMSModel/hybrid_evaluation/cache.py
  • data_science/SMSModel/hybrid_evaluation/metrics.py
  • data_science/SMSModel/hybrid_evaluation/models.py
  • data_science/SMSModel/hybrid_evaluation/reporting.py
  • data_science/SMSModel/hybrid_evaluation/runner.py
  • data_science/SMSModel/reports/hybrid_evaluation/comparison_report.json
  • data_science/SMSModel/reports/hybrid_evaluation/comparison_report.md
  • data_science/SMSModel/reports/hybrid_evaluation/evaluation_records.json
  • data_science/SMSModel/run_hybrid_evaluation.py
  • tests/analysis/test_hybrid_policy.py
  • tests/analysis/test_router.py
  • tests/analysis/text/test_hybrid_llm_failure_integration.py
  • tests/analysis/text/test_stacking_analyzer.py
  • tests/data_science/SMSModel/hybrid_evaluation/test_cache.py
  • tests/data_science/SMSModel/hybrid_evaluation/test_metrics.py
  • tests/data_science/SMSModel/hybrid_evaluation/test_reporting.py
  • tests/data_science/SMSModel/hybrid_evaluation/test_runner.py

Comment thread .gitignore
Comment on lines +143 to +148
"llm_usage": {
"input_tokens": None,
"output_tokens": None,
},
"llm_latency_ms": None,
"llm_from_cache": False,

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how text analysis fields reach the API response and whether legacy aliases cover the new keys.
rg -n -C4 'llm_usage|llm_latency_ms|llm_from_cache' app tests

# Inspect the legacy alias helper.
ast-grep run --pattern 'def _with_legacy_aliases($$$):
  $$$' --lang python app/analysis/text/hybrid_analyzer.py

# Locate response schemas for text analysis.
rg -n -C4 'llm_called|gemini_called' app --glob '!**/text/hybrid_analyzer.py'

Repository: SafeFam/SafeFam_AI

Length of output: 7022


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- result factory ---'
sed -n '100,215p' app/infrastructure/rabbitmq/result_factory.py
printf '%s\n' '--- response model/schema candidates ---'
rg -n -C3 'TextAnalysis|llmProvider|llmModel|geminiCalled|decisionSource|routingReason|llmCalled' app tests --glob '*.py'
printf '%s\n' '--- evaluation token extraction ---'
rg -n -C5 'input_tokens|output_tokens|llm_usage|usage' app tests --glob '*.py'

Repository: SafeFam/SafeFam_AI

Length of output: 48072


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- TextAnalysisDetail schema ---'
sed -n '104,148p' app/infrastructure/rabbitmq/schemas.py
printf '%s\n' '--- evaluation runner implementation ---'
sed -n '1,240p' app/data_science/SMSModel/hybrid_evaluation/runner.py 2>/dev/null || true
sed -n '1,240p' tests/data_science/SMSModel/hybrid_evaluation/test_runner.py
printf '%s\n' '--- runner and analyzer call sites ---'
rg -n -C5 'run_sample|run_batch|hybrid_analyzer|input_tokens|output_tokens|llm_usage' app tests/data_science --glob '*.py'
printf '%s\n' '--- serialization configuration ---'
rg -n -C4 'model_dump|model_config|extra=|AnalysisResultPayload|textAnalysis' app/infrastructure app --glob '*.py'

Repository: SafeFam/SafeFam_AI

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- evaluation source files ---'
git ls-files | grep -E '(^|/)(data_science|hybrid_evaluation)(/|.*\.py$)' | head -80
printf '%s\n' '--- token extraction definitions ---'
rg -n -C8 'class HybridEvaluationRunner|def evaluate_one|input_tokens|output_tokens|llm_usage|llm_analyzer' . --glob '*.py' --glob '!tests/**'
printf '%s\n' '--- direct API mapping tests ---'
sed -n '1,190p' tests/infrastructure/rabbitmq/test_result_factory.py
printf '%s\n' '--- package exports ---'
find . -path '*hybrid_evaluation*' -maxdepth 8 -type f -name '*.py' -print

Repository: SafeFam/SafeFam_AI

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- analysis response schemas ---'
rg -n -C4 'class .*Response|text_analysis|textAnalysis|llm' app/analysis/schemas.py app/api app --glob '*.py' | head -240
printf '%s\n' '--- runner token extraction and record construction ---'
sed -n '82,225p' data_science/SMSModel/hybrid_evaluation/runner.py
printf '%s\n' '--- result factory telemetry assertions ---'
rg -n -C5 'TextAnalysisDetail|llmCalled|llmProvider|decisionSource|model_dump|payload' tests/infrastructure/rabbitmq/test_result_factory.py

Repository: SafeFam/SafeFam_AI

Length of output: 26356


🏁 Script executed:

#!/bin/bash
set -e
rg -n -C6 'SmishingAnalysisResponse|text_analysis|build_payload|AnalysisResultEventFactory|analyze\(' app --glob '*.py' | head -320

Repository: SafeFam/SafeFam_AI

Length of output: 18670


Map telemetry into the published result contract.

The direct /analyze response forwards text_analysis as a dictionary. The RabbitMQ result contract does not. TextAnalysisDetail forbids extra fields, and result_factory.py does not map llm_usage, llm_latency_ms, or llm_from_cache; the published payload drops them. Add explicit contract fields and mappings. HybridEvaluationRunner reads the analyzer output directly, so this omission does not cause its token values to become None.

🤖 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/text/hybrid_analyzer.py` around lines 143 - 148, Add llm_usage,
llm_latency_ms, and llm_from_cache to the published TextAnalysisDetail contract,
then update result_factory.py to explicitly map these fields from the analyzer
output into the RabbitMQ payload. Preserve the direct /analyze response and
HybridEvaluationRunner behavior.

Comment on lines +1 to +19
{
"created_at": "2026-08-12T15:08:55.703054+00:00",
"llm_phishing_score": 40,
"schema_version": 1,
"selection": {
"f2": 0.9544159544159544,
"llm_call_count": 51,
"llm_call_rate": 0.4146341463414634,
"normal_probability_max": 0.08716666259958943,
"phishing_probability_min": 0.59,
"precision": 0.8072289156626506,
"recall": 1.0,
"target_recall": 0.95,
"target_recall_met": true,
"validation_count": 123
},
"source_split": "validation",
"split_manifest": "sms_split_v1.csv"
}

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

Record the model identity that these thresholds depend on.

normal_probability_max and phishing_probability_min are probability bands of one specific trained stacking model. The file records split_manifest and source_split, but no model identifier, model threshold, or artifact fingerprint. After a retrain, the frozen policy stays syntactically valid and the loader accepts stale thresholds silently.

Add the model provenance so that the loader can reject a policy that does not match the current model.

🛡️ Proposed provenance fields
   "source_split": "validation",
-  "split_manifest": "sms_split_v1.csv"
+  "split_manifest": "sms_split_v1.csv",
+  "model_name": "stacking_phishing_classifier",
+  "model_threshold": 0.08844759684377157
 }
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 8-8: Possible credit card number (PAN) detected in source code. Credit card numbers should never be hardcoded or stored in source files. Use a secrets manager or tokenization service instead.

(coderabbit.pii.credit-card-number)

🤖 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/artifacts/stacking/hybrid_policy.json` around lines 1 -
19, Add model provenance fields to the policy artifact represented by the
stacking policy JSON, including a stable model identifier or artifact
fingerprint and the model threshold used to derive the probability bands. Update
the policy loader’s validation to compare these values with the current model
and reject mismatches instead of accepting stale thresholds; preserve the
existing split metadata and threshold-selection fields.

Comment thread data_science/SMSModel/hybrid_evaluation/__init__.py
Comment thread data_science/SMSModel/hybrid_evaluation/cache.py
Comment thread data_science/SMSModel/hybrid_evaluation/reporting.py
Comment thread data_science/SMSModel/hybrid_evaluation/runner.py
Comment thread data_science/SMSModel/run_hybrid_evaluation.py
Comment thread tests/analysis/text/test_hybrid_llm_failure_integration.py Outdated
@pearseona pearseona added the test Adds or updates test, test data, or evaluation workflows label Aug 13, 2026
@pearseona
pearseona merged commit c0afa0f into develop Aug 13, 2026
3 checks passed
@pearseona pearseona changed the title Test(#37): Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 Test(#37): Bedrock Claude 하이브리드 성능·비용 평가 및 fallback 검증 (3/3) Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Adds or updates test, test data, or evaluation workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant