Skip to content

Feat(#37): Gemini 조건부 호출 및 하이브리드 텍스트 분석 파이프라인 구현 (2/3) - #59

Merged
pearseona merged 10 commits into
developfrom
feat/37-conditional-gemini
Aug 11, 2026
Merged

Feat(#37): Gemini 조건부 호출 및 하이브리드 텍스트 분석 파이프라인 구현 (2/3)#59
pearseona merged 10 commits into
developfrom
feat/37-conditional-gemini

Conversation

@pearseona

@pearseona pearseona commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📝 개요

PR #52에서 구현한 Stacking 자체 모델의 위험 확률을 기반으로, 자체 모델의 예측이 불확실한 메시지만 Gemini에 재검증을 요청하는 하이브리드 텍스트 분석 파이프라인을 구현했습니다.

Stacking 위험 확률이 확신 구간에 포함되면 자체 모델 결과를 즉시 사용하고, 두 임계값 사이의 불확실 구간에서는 Gemini 분석 결과를 최종 판정으로 사용합니다.

Gemini 호출 실패, timeout 또는 응답 파싱 실패 시에는 Stacking 결과로 fallback하며, 두 분석 엔진이 모두 실패한 경우 UNKNOWN으로 처리해 정상 메시지로 오판하는 fail-open을 방지했습니다.

🔗 관련 이슈

🎯 주요 변경 사항

Gemini 조건부 호출 정책 구현

  • Stacking의 risk_probability를 기준으로 정상·불확실·위험 구간 분류
  • 정상 확신 구간과 위험 확신 구간에서는 Gemini 호출 생략
  • 두 임계값 사이의 불확실 구간에서만 Gemini 재검증 수행
  • Stacking을 사용할 수 없는 경우 Gemini fallback 수행
  • 잘못된 임계값과 유효하지 않은 Stacking 확률 검증

하이브리드 최종 판정 구현

  • Stacking과 Gemini 분석 결과를 기존 텍스트 분석 응답 형식으로 정규화
  • Gemini를 호출하지 않은 경우 Stacking 결과를 최종 판정으로 사용
  • Gemini 호출에 성공한 경우 Gemini 결과를 최종 판정으로 사용
  • 최종 판정 출처, 라우팅 사유 및 fallback 적용 여부 기록

장애 fallback 구현

  • Gemini timeout, API 오류, Rate Limit 및 응답 파싱 실패 처리
  • Gemini 실패 시 사용 가능한 Stacking 결과로 fallback
  • Gemini 분석기가 예상하지 못한 예외를 발생시켜도 Stacking 결과 유지
  • Stacking 분석기가 예외를 발생시키면 Gemini fallback 수행
  • 두 엔진이 모두 실패한 경우 UNKNOWN
    ALL_TEXT_ENGINES_UNAVAILABLE 반환
  • 로그에 API Key와 원문 메시지가 포함되지 않도록 오류 유형만 기록

임계값 선정 도구 구현

  • Validation 데이터의 Recall, F2 및 Gemini 호출률을 기준으로 임계값 탐색
  • Gemini 검증 결과를 fingerprint 기준으로 캐시
  • 중단 후 재실행 시 성공한 Gemini 호출 결과 재사용
  • 모든 Validation 결과가 수집되기 전에는 임계값 선정을 차단
  • 선정 결과를 별도 report와 Stacking metadata에 저장

분석 응답 및 RabbitMQ 연동

  • 자체 모델 점수와 신뢰도 추가
  • Gemini 호출 여부 추가
  • 최종 판정 출처와 라우팅 사유 추가
  • fallback 적용 여부 추가
  • TEXT:STACKING, TEXT:GEMINI, TEXT 실패 상태 분류
  • COMPLETED, PARTIAL, FAILED 결과 이벤트 반영
  • SafeFam_BE DTO에서 신규 하이브리드 필드를 수신하도록 별도 연동

📊 검증 결과

지표 결과
Stacking Validation Recall 1.0000
Stacking Validation F2 0.9153
Gemini Validation 전체 데이터 123건
Gemini 캐시 수집 17건
사용 가능한 Gemini 결과 13건
정상 확신 구간 상한 미확정
위험 확신 구간 하한 미확정
Hybrid Validation Recall 미확정
Hybrid Validation F2 미확정
예상 Gemini 호출률 미확정
로컬 테스트 434 passed, 3 deselected

Gemini API 호출 한도 초과로 전체 Validation 예측 수집을 완료하지 못했습니다.
최종 임계값과 Gemini 호출률은 전체 결과 수집 후 확정할 예정입니다.

⚠️ 현재 제한 사항

  • 기본 임계값은 아직 최종 Validation 결과가 아닙니다.
  • Gemini API 한도 초과로 전체 123건에 대한 하이브리드 성능 비교가 완료되지 않았습니다.
  • Gemini 실호출을 제외한 정책, fallback, 서비스 및 이벤트 테스트는 완료했습니다.

✅ PR 체크리스트

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

Summary by CodeRabbit

  • New Features

    • Added hybrid message analysis combining self-model results with Gemini review when confidence is uncertain.
    • Added configurable thresholds to balance detection confidence and external review usage.
    • Added detailed results, including scores, confidence, routing decisions, and fallback status.
    • Added threshold selection tooling based on validation performance.
  • Bug Fixes

    • Improved failure classification and fail-safe handling when analysis services are unavailable.
  • Documentation

    • Added documentation for PII masking, scoring pipelines, and model training workflows.
  • Tests

    • Expanded coverage for hybrid routing, fallback behavior, threshold validation, and error handling.

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

coderabbitai Bot commented Aug 11, 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: 31 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: fd06af44-9100-46fd-9b4a-0f4b62eadf14

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8e737 and 1b4c91c.

📒 Files selected for processing (11)
  • app/analysis/service.py
  • app/analysis/text/hybrid_analyzer.py
  • app/core/config.py
  • app/infrastructure/rabbitmq/result_factory.py
  • data_science/SMSModel/run_hybrid_threshold_selection.py
  • docs/SCORING_PIPELINE_CHANGES.md
  • docs/TRAINING_FLOW.md
  • tests/analysis/test_service.py
  • tests/analysis/text/test_hybrid_analyzer.py
  • tests/data_science/SMSModel/test_hybrid_threshold_selection.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
📝 Walkthrough

Walkthrough

The PR replaces legacy text-analysis orchestration with conditional Stacking/Gemini routing. It adds threshold selection, fail-safe engine handling, pipeline integration, RabbitMQ metadata, validation artifacts, tests, and documentation.

Changes

Hybrid stacking and Gemini analysis

Layer / File(s) Summary
Hybrid threshold selection
data_science/SMSModel/modeling/hybrid_thresholds.py, data_science/SMSModel/run_hybrid_threshold_selection.py, data_science/SMSModel/artifacts/stacking/*, tests/data_science/SMSModel/modeling/*
The threshold selector validates inputs, evaluates candidate boundaries, filters by recall, ranks by F2 and Gemini call rate, and writes selected policy metadata. The CLI supports cached or newly collected Gemini validation scores.
Conditional routing and hybrid text analysis
app/analysis/hybrid_policy.py, app/analysis/text/hybrid_analyzer.py, app/core/config.py, tests/analysis/text/*, tests/analysis/test_hybrid_policy.py, tests/core/test_config.py
The runtime routes valid Stacking results to self-classification, Gemini review, or Gemini fallback. Invalid or unavailable engine results are normalized, and probability thresholds are validated.
Pipeline execution and result reporting
app/analysis/service.py, app/analysis/execution.py, app/infrastructure/rabbitmq/*, tests/analysis/*, tests/infrastructure/rabbitmq/*
The service uses HybridTextAnalyzer, preserves parallel URL analysis, updates failure classification and fail-safe scoring, and publishes Stacking/Gemini routing metadata.
Pipeline documentation
docs/PII_MASKING.md, docs/SCORING_PIPELINE_CHANGES.md, docs/TRAINING_FLOW.md
The documentation records PII masking, scoring behavior, and SMS and Voice training flows.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SmishingAnalysisService
  participant HybridTextAnalyzer
  participant StackingAnalyzer
  participant ConditionalGeminiPolicy
  participant GeminiAnalyzer
  participant RabbitMQResultFactory

  Client->>SmishingAnalysisService: submit message text
  SmishingAnalysisService->>HybridTextAnalyzer: analyze text
  HybridTextAnalyzer->>StackingAnalyzer: run Stacking analysis
  StackingAnalyzer-->>ConditionalGeminiPolicy: return probability and availability
  ConditionalGeminiPolicy-->>HybridTextAnalyzer: return routing decision
  HybridTextAnalyzer->>GeminiAnalyzer: run Gemini when required
  GeminiAnalyzer-->>HybridTextAnalyzer: return result or normalized failure
  HybridTextAnalyzer-->>SmishingAnalysisService: return hybrid analysis metadata
  SmishingAnalysisService->>RabbitMQResultFactory: build text analysis detail
  RabbitMQResultFactory-->>Client: publish analysis result metadata
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% 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 The title clearly summarizes the main change: conditional Gemini invocation and hybrid text analysis pipeline implementation.
✨ 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 feat/37-conditional-gemini

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: 13

🧹 Nitpick comments (11)
data_science/SMSModel/run_hybrid_threshold_selection.py (2)

101-176: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Document the trust boundary of the checksum check.

The digest comes from metadata.json, which sits in the same directory as model.joblib. Anyone who can replace the artifact can also replace the expected digest. The check protects against corruption and stale artifacts. It does not protect against tampering, because joblib.load unpickles and can execute arbitrary code. Add a short comment that states this, so a later reader does not treat the check as a security control.

🤖 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_threshold_selection.py` around lines 101 -
176, The checksum validation in _load_stacking_classifier only detects
corruption or stale artifacts, not tampering, because model_sha256 is read from
the colocated metadata.json and joblib.load can execute pickle code. Add a short
comment immediately before the checksum comparison or deserialization
documenting this trust boundary and explicitly stating that the check is not a
security control.

Source: Linters/SAST tools


441-464: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the Gemini phishing score constant.

The value 40 is hardcoded at line 457 and at line 527, and it is also the default of gemini_phishing_score in select_hybrid_thresholds. If the SAFE/SUSPICIOUS boundary changes, the report can disagree with the value used for selection. Define one module-level constant and use it in both places.

♻️ Proposed refactor
+GEMINI_PHISHING_SCORE = 40
+
 def _write_policy_report(
     selection: HybridThresholdSelection,
 ) -> None:
@@
-        "gemini_phishing_score": 40,
+        "gemini_phishing_score": GEMINI_PHISHING_SCORE,
         target_recall=target_recall,
-
-        gemini_phishing_score=40,
+        gemini_phishing_score=GEMINI_PHISHING_SCORE,
     )
🤖 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_threshold_selection.py` around lines 441 -
464, Define a single module-level constant for the Gemini phishing score, then
replace the hardcoded 40 in _write_policy_report and the other usage near line
527, and use the same constant as the default gemini_phishing_score in
select_hybrid_thresholds so selection and reporting remain consistent.
data_science/SMSModel/modeling/hybrid_thresholds.py (1)

185-236: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Reduce the search cost of the threshold grid.

candidates contains 101 + n unique values. The nested loop evaluates about (101 + n)^2 / 2 pairs, and each accepted pair calls three scikit-learn metric functions over n samples. The cost grows as O(n^3). With a few thousand validation rows the selection run becomes impractical. The committed cache holds only 17 rows, so the cost is hidden today.

Two options:

  • Restrict the probability candidates to quantiles (for example 200 evenly spaced quantiles) instead of every observed probability.
  • Compute the confusion counts with vectorized NumPy over sorted probabilities and derive recall, precision, and F2 directly, instead of calling recall_score, precision_score, and fbeta_score inside the loop.
♻️ Example: bound the candidate grid
     # 고정 간격 후보와 실제 확률값을 함께 사용
+    MAX_PROBABILITY_CANDIDATES = 200
+
+    probability_candidates = np.quantile(
+        probabilities,
+        np.linspace(
+            0.0,
+            1.0,
+            MAX_PROBABILITY_CANDIDATES,
+        ),
+    )
+
     candidates = np.unique(
         np.concatenate(
             [
                 np.linspace(
                     0.0,
                     1.0,
                     101,
                 ),
-                probabilities,
+                probability_candidates,
             ]
         )
     )
🤖 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/hybrid_thresholds.py` around lines 185 - 236,
Reduce the threshold-search cost in the candidate generation and nested loops by
bounding observed probability candidates to a fixed-size quantile grid (for
example, at most 200 evenly spaced quantiles) while retaining the fixed 0.0–1.0
boundaries and uniqueness. Preserve valid normal_max/phishing_min ordering and
the existing metric-based selection behavior.
tests/analysis/test_execution.py (1)

35-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding the remaining hybrid fields to the builder.

_text_analysis omits decision_source, routing_reason, fallback_applied, and self_model.confidence. The fixtures in tests/infrastructure/rabbitmq/test_result_factory.py and tests/infrastructure/rabbitmq/test_consumer.py include them. classify_execution does not read those keys today, so no test fails. Adding them keeps one shared shape for the hybrid text response and protects the classifier tests if the classifier later reads routing metadata.

🤖 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_execution.py` around lines 35 - 56, Extend the
_text_analysis test-data builder to include decision_source, routing_reason, and
fallback_applied in the result payload, plus confidence under self_model, using
the shared hybrid response values and defaults established by the RabbitMQ
fixtures. Preserve the existing score, error, and Gemini-related fields while
keeping the returned schema consistent for classifier tests.
tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py (1)

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

Add coverage for the unreachable-target branch.

The parametrized cases cover the ValueError paths in _validate_inputs. The RuntimeError at data_science/SMSModel/modeling/hybrid_thresholds.py lines 310-314 has no test. That branch decides whether an unreachable recall target stops the workflow or silently returns a weak policy. Add a case where no threshold pair reaches target_recall, for example labels that Gemini and stacking both misclassify.

The label-set check at lines 121-130 is also untested. A single-label array is a cheap case to add.

🤖 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/modeling/test_hybrid_thresholds.py` around lines
105 - 152, Add parametrized coverage in test_rejects_invalid_inputs for a
single-label labels array to exercise the label-set validation, and add a
separate test for select_hybrid_thresholds where Gemini and stacking both
misclassify so no threshold pair reaches target_recall and the
unreachable-target RuntimeError is asserted. Use the existing test helpers and
invocation style.
tests/infrastructure/rabbitmq/test_result_factory.py (1)

144-153: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for the fallback and unavailable methods.

These assertions cover the STACKING_GEMINI path only. TextAnalysisMethod in app/infrastructure/rabbitmq/schemas.py also defines STACKING, STACKING_FALLBACK, and UNAVAILABLE. The PR objectives describe Gemini timeouts, API errors, and rate limits that fall back to Stacking, and an ALL_TEXT_ENGINES_UNAVAILABLE outcome. The mapping from text-analysis metadata to those three enum values is untested here, and it is the metadata that downstream consumers read.

Add cases for a fallback result (fallback_applied: True) and for an all-engines-unavailable result.

🤖 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/infrastructure/rabbitmq/test_result_factory.py` around lines 144 - 153,
Add test cases alongside the existing STACKING_GEMINI assertions to cover
metadata mapping for a fallback result with fallback_applied=True, expecting
TextAnalysisMethod.STACKING_FALLBACK, and an all-engines-unavailable result,
expecting TextAnalysisMethod.UNAVAILABLE and the corresponding outcome metadata.
Reuse the existing event factory and assertion style so downstream-readable
fields are verified for both paths.
tests/analysis/text/test_hybrid_analyzer.py (1)

171-192: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the upstream exception text does not leak into the result.

The test uses the message "secret upstream detail" to model sensitive upstream detail. It does not verify that the message is absent from the returned payload. Add an explicit assertion so a future change that surfaces str(exception) fails this test.

💚 Proposed addition
     assert (
         result["gemini"]["error_message"]
         == "GEMINI_ANALYZER_FAILED"
     )
+    assert "secret upstream detail" not in str(result)
🤖 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_analyzer.py` around lines 171 - 192, Update
test_uses_stacking_when_gemini_raises to assert that the returned result
contains no occurrence of the sensitive text "secret upstream detail",
especially within result["gemini"]["error_message"]. Preserve the existing
fallback and sanitized error-code assertions.
app/analysis/text/hybrid_analyzer.py (2)

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

Silence the BLE001 warnings for the intentional fail-safe catches.

Both broad catches are deliberate. They normalize engine failures instead of propagating them. app/analysis/text/stacking_analyzer.py already marks the same pattern with # noqa: BLE001. Apply the same marker here for consistency with the linter configuration.

♻️ Proposed change
-        except Exception as exception:
+        except Exception as exception:  # noqa: BLE001

Also applies to: 172-172

🤖 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` at line 106, Add the # noqa: BLE001
marker to both intentional broad Exception handlers in the relevant analyzer
methods, including the catches around the lines represented by “except Exception
as exception.” Match the existing suppression pattern used in
stacking_analyzer.py without changing the fail-safe behavior.

Source: Linters/SAST tools


269-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use routing.decision.value instead of the hardcoded enum.

This branch runs only when stacking_analysis["is_available"] is falsy. ConditionalGeminiPolicy.route already returns GEMINI_FALLBACK for that input, so the hardcoded value duplicates the policy decision. The adjacent routing_reason already reads from routing. Reading both fields from routing keeps the metadata consistent if the policy adds a new unavailable-model decision later.

♻️ Proposed change
-            "routing_decision": (
-                HybridRoutingDecision
-                .GEMINI_FALLBACK
-                .value
-            ),
+            "routing_decision": (
+                routing.decision.value
+            ),

Remove the now-unused HybridRoutingDecision import if no other reference remains.

🤖 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 269 - 273, Update the
unavailable-model branch in the hybrid analysis flow to set routing_decision
from routing.decision.value, matching the adjacent routing_reason metadata and
preserving the policy’s returned decision. Remove the HybridRoutingDecision
import if no other references remain.
tests/analysis/test_hybrid_policy.py (1)

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

Add tests for the two uncovered guard branches and the threshold boundaries.

ConditionalGeminiPolicy.route has two fail-safe branches that no test exercises: STACKING_PROBABILITY_UNAVAILABLE (line 92 of app/analysis/hybrid_policy.py) and INVALID_STACKING_PROBABILITY (line 103). Both prevent a malformed stacking result from being treated as normal. The boundary values normal_max and phishing_min are also untested, and the comparisons use <= and >=.

💚 Proposed additional tests
`@pytest.mark.parametrize`(
    "probability",
    [None, "0.5", float("nan"), 1.5, -0.1],
)
def test_falls_back_to_gemini_for_unusable_probability(
    policy: ConditionalGeminiPolicy,
    probability,
) -> None:
    result = policy.route(
        {
            "engine": "stacking",
            "is_available": True,
            "result": {"risk_probability": probability},
        }
    )

    assert (
        result.decision
        == HybridRoutingDecision.GEMINI_FALLBACK
    )
    assert result.should_call_gemini is True


`@pytest.mark.parametrize`(
    ("probability", "expected"),
    [
        (0.2, HybridRoutingDecision.SELF_MODEL_NORMAL),
        (0.8, HybridRoutingDecision.SELF_MODEL_PHISHING),
    ],
)
def test_threshold_boundaries_are_inclusive(
    policy: ConditionalGeminiPolicy,
    probability: float,
    expected: HybridRoutingDecision,
) -> None:
    result = policy.route(
        build_stacking_result(probability)
    )

    assert result.decision == expected
    assert result.should_call_gemini is False
🤖 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_hybrid_policy.py` around lines 82 - 119, Add tests in
tests/analysis/test_hybrid_policy.py covering ConditionalGeminiPolicy.route’s
unavailable and invalid stacking-probability fallback branches with None,
nonnumeric, NaN, and out-of-range values, asserting GEMINI_FALLBACK and
should_call_gemini=True. Also add boundary tests for HybridThresholds using
probabilities equal to normal_max and phishing_min, asserting the inclusive
SELF_MODEL_NORMAL and SELF_MODEL_PHISHING decisions without Gemini.
tests/analysis/test_service.py (1)

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

Assert the exact final score for the Gemini escalation path.

assert result.final_score >= 40 passes for a wide range of values. It does not distinguish the Gemini score of 90 from the blended text-track score that RiskScoringEngine currently computes from self_model_score=50 and selected_score=90. See the related comment on app/analysis/service.py Lines 331-336.

Assert the exact expected score and the text contribution. A precise assertion documents which score the pipeline applies and fails if the blending semantics change.

💚 Proposed change
     assert result.status == "SUCCESS"
     assert result.text_analysis["decision_source"] == "GEMINI"
     assert result.text_analysis["gemini_available"] is True
-    assert result.final_score >= 40
+    # 텍스트 트랙이 어떤 점수를 적용했는지 명시적으로 고정
+    assert result.contribution_breakdown.llm == <expected>
+    assert result.final_score == <expected>
🤖 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 120 - 123, Update the Gemini
escalation assertions in the test to verify the exact expected final_score
produced by the current blending logic, and assert the corresponding text
contribution as described by RiskScoringEngine. Replace the broad lower-bound
check while preserving the existing status, decision_source, and
gemini_available assertions.
🤖 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/service.py`:
- Around line 331-336: The service text-track call at app/analysis/service.py
lines 331-336 should pass only the HybridTextAnalyzer-selected score as the
single text signal, remove the raw naive_bayes_score=self_model_score argument,
and derive llm_available from whether a selected score exists. Update
tests/analysis/test_service.py lines 120-123 to assert the exact
result.final_score and result.contribution_breakdown.llm values, pinning the
applied selected text score.
- Around line 136-146: Update the asyncio.gather flow around text_task and
url_task to collect both task outcomes and handle exceptions explicitly. When
either task fails, cancel the sibling task, await its completion, and preserve
the existing fail-safe ERROR response; ensure both task results or exceptions
are retrieved so no background work or unhandled task warning remains.

In `@app/analysis/text/hybrid_analyzer.py`:
- Around line 104-105: Update the async analyze flow around
self.stacking_analyzer in HybridAnalyzer.analyze to execute the synchronous
callable via asyncio.to_thread, adding the asyncio import. Preserve the existing
arguments, result handling, and exception behavior while preventing blocking of
the event loop.

In `@app/core/config.py`:
- Around line 20-32: Update the default values for
STACKING_NORMAL_PROBABILITY_MAX and STACKING_PHISHING_PROBABILITY_MIN to
conservative, non-degenerate thresholds such as 0.1 and 0.9, while preserving
their existing 0–1 validation. Ensure the defaults no longer make nearly every
probability fall into the uncertain band.

In `@app/infrastructure/rabbitmq/result_factory.py`:
- Around line 163-165: Update the result mapping around selfModelConfidence and
add a _confidence helper next to _integer_score. Have the helper reject
booleans, non-numeric values, and NaN as None, while converting valid numbers to
float and clamping finite values to the 0.0–1.0 range before assigning
selfModelConfidence.

In `@data_science/SMSModel/artifacts/stacking/gemini_validation_predictions.json`:
- Around line 1-108: Document that the committed gemini_validation_predictions
cache is intentionally partial because of Gemini quota limits, including that
unavailable entries prevent threshold selection and are not a regression. Add
this note to the relevant PR or documentation while leaving the
fingerprint-and-score cache unchanged.

In `@data_science/SMSModel/run_hybrid_threshold_selection.py`:
- Around line 351-370: Update the score validation in the available calculation
to explicitly reject bool values while continuing to accept only integer scores
in the existing 0–100 range; preserve the current integer risk_score contract
and downstream _ordered_gemini_scores behavior.

In `@docs/PII_MASKING.md`:
- Around line 24-26: Update the deployment sequence in the SafeFam_BE/SafeFam_AI
token-format migration instructions to deploy SafeFam_AI before SafeFam_BE.
Alternatively, require both token formats to be supported by SafeFam_AI before
deploying the new-emitting SafeFam_BE version.

In `@docs/SCORING_PIPELINE_CHANGES.md`:
- Around line 109-120: Update docs/SCORING_PIPELINE_CHANGES.md lines 109-120 to
distinguish final-grade thresholds (0–39 LOW, 40–69 MEDIUM, 70–100 HIGH) from
the separate thresholds that route uncertain results to Gemini, documenting the
selected hybrid policy without conflating the two. Update docs/TRAINING_FLOW.md
lines 74-82 to include Recall, F2, Gemini call rate, fingerprint caching, policy
metadata, and the provisional validation state.
- Around line 123-136: Update the “테스트 현황” section to report the complete
validation result, including 434 passed and 3 deselected tests, the exact test
scope, and the Gemini quota limitation. Mark the final thresholds and Gemini
call rate as provisional until those limitations are resolved, while retaining
the existing list of relevant tests and out-of-scope files.
- Around line 5-17: Update the documentation to match the current
Stacking/Gemini contract: in docs/SCORING_PIPELINE_CHANGES.md lines 5-17,
replace the Naive Bayes-first and SAFE-only routing diagram with high-confidence
and uncertain-result routing; in docs/PII_MASKING.md lines 18-20, identify the
Stacking hybrid analyzer and Gemini uncertainty path; and in
docs/TRAINING_FLOW.md line 172, replace Claude and medium-only escalation
terminology with Gemini and the current routing policy.
- Around line 102-105: Update the scoring documentation around the VirusTotal
formula to state that raw_score is normalized to 0–1, while
app/infrastructure/rabbitmq/result_factory.py:_url_score converts it to 0–100
for external output. Add boundary examples for raw scores 0 and 1.0, plus the
zero-engines case, and clarify that callers must not apply the conversion twice.

In `@docs/TRAINING_FLOW.md`:
- Around line 113-118: The documentation for generate_voice_data.py must clarify
that its precomputed 68/16/16 split assignment is informational because
_leak_free_split() reassigns splits; alternatively remove the precomputed split
claim. Explicitly state whether each 15% allocation is calculated from the
original dataset or from the remaining subset, consistently in both affected
sections.

---

Nitpick comments:
In `@app/analysis/text/hybrid_analyzer.py`:
- Line 106: Add the # noqa: BLE001 marker to both intentional broad Exception
handlers in the relevant analyzer methods, including the catches around the
lines represented by “except Exception as exception.” Match the existing
suppression pattern used in stacking_analyzer.py without changing the fail-safe
behavior.
- Around line 269-273: Update the unavailable-model branch in the hybrid
analysis flow to set routing_decision from routing.decision.value, matching the
adjacent routing_reason metadata and preserving the policy’s returned decision.
Remove the HybridRoutingDecision import if no other references remain.

In `@data_science/SMSModel/modeling/hybrid_thresholds.py`:
- Around line 185-236: Reduce the threshold-search cost in the candidate
generation and nested loops by bounding observed probability candidates to a
fixed-size quantile grid (for example, at most 200 evenly spaced quantiles)
while retaining the fixed 0.0–1.0 boundaries and uniqueness. Preserve valid
normal_max/phishing_min ordering and the existing metric-based selection
behavior.

In `@data_science/SMSModel/run_hybrid_threshold_selection.py`:
- Around line 101-176: The checksum validation in _load_stacking_classifier only
detects corruption or stale artifacts, not tampering, because model_sha256 is
read from the colocated metadata.json and joblib.load can execute pickle code.
Add a short comment immediately before the checksum comparison or
deserialization documenting this trust boundary and explicitly stating that the
check is not a security control.
- Around line 441-464: Define a single module-level constant for the Gemini
phishing score, then replace the hardcoded 40 in _write_policy_report and the
other usage near line 527, and use the same constant as the default
gemini_phishing_score in select_hybrid_thresholds so selection and reporting
remain consistent.

In `@tests/analysis/test_execution.py`:
- Around line 35-56: Extend the _text_analysis test-data builder to include
decision_source, routing_reason, and fallback_applied in the result payload,
plus confidence under self_model, using the shared hybrid response values and
defaults established by the RabbitMQ fixtures. Preserve the existing score,
error, and Gemini-related fields while keeping the returned schema consistent
for classifier tests.

In `@tests/analysis/test_hybrid_policy.py`:
- Around line 82-119: Add tests in tests/analysis/test_hybrid_policy.py covering
ConditionalGeminiPolicy.route’s unavailable and invalid stacking-probability
fallback branches with None, nonnumeric, NaN, and out-of-range values, asserting
GEMINI_FALLBACK and should_call_gemini=True. Also add boundary tests for
HybridThresholds using probabilities equal to normal_max and phishing_min,
asserting the inclusive SELF_MODEL_NORMAL and SELF_MODEL_PHISHING decisions
without Gemini.

In `@tests/analysis/test_service.py`:
- Around line 120-123: Update the Gemini escalation assertions in the test to
verify the exact expected final_score produced by the current blending logic,
and assert the corresponding text contribution as described by
RiskScoringEngine. Replace the broad lower-bound check while preserving the
existing status, decision_source, and gemini_available assertions.

In `@tests/analysis/text/test_hybrid_analyzer.py`:
- Around line 171-192: Update test_uses_stacking_when_gemini_raises to assert
that the returned result contains no occurrence of the sensitive text "secret
upstream detail", especially within result["gemini"]["error_message"]. Preserve
the existing fallback and sanitized error-code assertions.

In `@tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py`:
- Around line 105-152: Add parametrized coverage in test_rejects_invalid_inputs
for a single-label labels array to exercise the label-set validation, and add a
separate test for select_hybrid_thresholds where Gemini and stacking both
misclassify so no threshold pair reaches target_recall and the
unreachable-target RuntimeError is asserted. Use the existing test helpers and
invocation style.

In `@tests/infrastructure/rabbitmq/test_result_factory.py`:
- Around line 144-153: Add test cases alongside the existing STACKING_GEMINI
assertions to cover metadata mapping for a fallback result with
fallback_applied=True, expecting TextAnalysisMethod.STACKING_FALLBACK, and an
all-engines-unavailable result, expecting TextAnalysisMethod.UNAVAILABLE and the
corresponding outcome metadata. Reuse the existing event factory and assertion
style so downstream-readable fields are verified for both paths.
🪄 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: 5bba9692-de3c-4670-bef0-161d9095a7e9

📥 Commits

Reviewing files that changed from the base of the PR and between dc6b9fd and dc78eeb.

📒 Files selected for processing (22)
  • app/analysis/execution.py
  • app/analysis/hybrid_policy.py
  • app/analysis/service.py
  • app/analysis/text/hybrid_analyzer.py
  • app/core/config.py
  • app/infrastructure/rabbitmq/result_factory.py
  • app/infrastructure/rabbitmq/schemas.py
  • data_science/SMSModel/artifacts/stacking/gemini_validation_predictions.json
  • data_science/SMSModel/modeling/hybrid_thresholds.py
  • data_science/SMSModel/run_hybrid_threshold_selection.py
  • docs/PII_MASKING.md
  • docs/SCORING_PIPELINE_CHANGES.md
  • docs/TRAINING_FLOW.md
  • tests/analysis/test_execution.py
  • tests/analysis/test_hybrid_policy.py
  • tests/analysis/test_router.py
  • tests/analysis/test_service.py
  • tests/analysis/text/test_hybrid_analyzer.py
  • tests/core/test_config.py
  • tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py
  • tests/infrastructure/rabbitmq/test_consumer.py
  • tests/infrastructure/rabbitmq/test_result_factory.py

Comment thread app/analysis/service.py Outdated
Comment thread app/analysis/service.py Outdated
Comment thread app/analysis/text/hybrid_analyzer.py Outdated
Comment thread app/core/config.py
Comment thread app/infrastructure/rabbitmq/result_factory.py
Comment on lines +1 to +108
{
"predictions": [
{
"available": false,
"error_code": "GEMINI_VALIDATION_FAILED",
"risk_score": null,
"text_fingerprint": "289b21983f4d981cd429323483365d60e967d089656d930db86180446bbc44f0"
},
{
"available": true,
"error_code": null,
"risk_score": 92,
"text_fingerprint": "2ea8346732eddecebb3d5b0af2dadfe19eac847d63d1a54616e3c9780c0d0a9b"
},
{
"available": true,
"error_code": null,
"risk_score": 5,
"text_fingerprint": "301f545f30fe3a4b2ea8bf0be80d564782893f0137f7fc10645244a3d82eb833"
},
{
"available": true,
"error_code": null,
"risk_score": 92,
"text_fingerprint": "3256ba9d4ad81438c3a78ad2d9d9124a2f6db7d2ba33c934f2093f1ea2b55742"
},
{
"available": true,
"error_code": null,
"risk_score": 92,
"text_fingerprint": "354a654dd179372d1c17ae688d9cffbc5ff4afaddf944bffe01a178152368903"
},
{
"available": false,
"error_code": "GEMINI_VALIDATION_FAILED",
"risk_score": null,
"text_fingerprint": "3ee443e030ab5aff20c11eb1d2a3857f363d8fe4a989e99448b0e12e9069e216"
},
{
"available": true,
"error_code": null,
"risk_score": 85,
"text_fingerprint": "4142fcb037bf6abb39e3c8d499beba8b3f08276d63f3f041911c1ebd7a574222"
},
{
"available": true,
"error_code": null,
"risk_score": 5,
"text_fingerprint": "5580c99edd3218684721ed377d478d39496755e1956427c69ec48ccedee48db8"
},
{
"available": true,
"error_code": null,
"risk_score": 95,
"text_fingerprint": "638d116d425486fdd6c6d02756dbc9f8c32288aa05c6129adbcd6ef8ee8e127c"
},
{
"available": true,
"error_code": null,
"risk_score": 90,
"text_fingerprint": "6954d76bffa4df64d32eece72440e5da18f1f6546b2f98ec65b344bbdfac4056"
},
{
"available": true,
"error_code": null,
"risk_score": 10,
"text_fingerprint": "7243157b2c8f9fab1664cd2b01270de2d106b872c1620e2402a1c9329c2ba243"
},
{
"available": true,
"error_code": null,
"risk_score": 5,
"text_fingerprint": "828b133c682a90cf80dc66ddb563f0bad1add03d628f6ebb70e813269eba681f"
},
{
"available": true,
"error_code": null,
"risk_score": 75,
"text_fingerprint": "af129827c3283f2218baef8752bf17dc48159f820b6793985bfcd7c83213f83b"
},
{
"available": false,
"error_code": "GEMINI_VALIDATION_FAILED",
"risk_score": null,
"text_fingerprint": "d032c79c99bf3ca9213f7263f47f32cfd8526e911d5a849518b237e4517ad7ba"
},
{
"available": false,
"error_code": "GEMINI_VALIDATION_FAILED",
"risk_score": null,
"text_fingerprint": "d12d5c8c893c7770de85d622bb84933abd4c5b8dd6da76a497f7db729c9cb1ca"
},
{
"available": true,
"error_code": null,
"risk_score": 92,
"text_fingerprint": "eb8f6d7e5925e119738ba91c9b0ed18f2ecb8d9b2a3a3bff7ae366435b3681d4"
},
{
"available": true,
"error_code": null,
"risk_score": 90,
"text_fingerprint": "ee03ab85bde5b1e60c128fbd34eabecc0c854d63f30131db8df9f8071ba28de8"
}
],
"schema_version": 1,
"updated_at": "2026-08-11T05:19:14.067891+00:00"
}

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

The committed cache cannot produce thresholds in its current state.

The file holds 17 entries, and 4 of them have "available": false. _ordered_gemini_scores in data_science/SMSModel/run_hybrid_threshold_selection.py raises RuntimeError when any expected fingerprint is not available, and it also raises when the validation split contains fingerprints that are absent here. A run of run_hybrid_threshold_selection.py against the full validation split therefore fails before selection.

This matches the PR statement that validation is incomplete because of Gemini quota limits. The file is safe to commit, because it stores only fingerprints and scores. Record the partial state in the PR or in the docs so a later run is not treated as a regression.

🤖 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/gemini_validation_predictions.json`
around lines 1 - 108, Document that the committed gemini_validation_predictions
cache is intentionally partial because of Gemini quota limits, including that
unavailable entries prevent threshold selection and are not a regression. Add
this note to the relevant PR or documentation while leaving the
fingerprint-and-score cache unchanged.

Comment thread data_science/SMSModel/run_hybrid_threshold_selection.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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 13

🧹 Nitpick comments (11)
data_science/SMSModel/run_hybrid_threshold_selection.py (2)

101-176: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Document the trust boundary of the checksum check.

The digest comes from metadata.json, which sits in the same directory as model.joblib. Anyone who can replace the artifact can also replace the expected digest. The check protects against corruption and stale artifacts. It does not protect against tampering, because joblib.load unpickles and can execute arbitrary code. Add a short comment that states this, so a later reader does not treat the check as a security control.

🤖 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_threshold_selection.py` around lines 101 -
176, The checksum validation in _load_stacking_classifier only detects
corruption or stale artifacts, not tampering, because model_sha256 is read from
the colocated metadata.json and joblib.load can execute pickle code. Add a short
comment immediately before the checksum comparison or deserialization
documenting this trust boundary and explicitly stating that the check is not a
security control.

Source: Linters/SAST tools


441-464: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the Gemini phishing score constant.

The value 40 is hardcoded at line 457 and at line 527, and it is also the default of gemini_phishing_score in select_hybrid_thresholds. If the SAFE/SUSPICIOUS boundary changes, the report can disagree with the value used for selection. Define one module-level constant and use it in both places.

♻️ Proposed refactor
+GEMINI_PHISHING_SCORE = 40
+
 def _write_policy_report(
     selection: HybridThresholdSelection,
 ) -> None:
@@
-        "gemini_phishing_score": 40,
+        "gemini_phishing_score": GEMINI_PHISHING_SCORE,
         target_recall=target_recall,
-
-        gemini_phishing_score=40,
+        gemini_phishing_score=GEMINI_PHISHING_SCORE,
     )
🤖 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_threshold_selection.py` around lines 441 -
464, Define a single module-level constant for the Gemini phishing score, then
replace the hardcoded 40 in _write_policy_report and the other usage near line
527, and use the same constant as the default gemini_phishing_score in
select_hybrid_thresholds so selection and reporting remain consistent.
data_science/SMSModel/modeling/hybrid_thresholds.py (1)

185-236: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Reduce the search cost of the threshold grid.

candidates contains 101 + n unique values. The nested loop evaluates about (101 + n)^2 / 2 pairs, and each accepted pair calls three scikit-learn metric functions over n samples. The cost grows as O(n^3). With a few thousand validation rows the selection run becomes impractical. The committed cache holds only 17 rows, so the cost is hidden today.

Two options:

  • Restrict the probability candidates to quantiles (for example 200 evenly spaced quantiles) instead of every observed probability.
  • Compute the confusion counts with vectorized NumPy over sorted probabilities and derive recall, precision, and F2 directly, instead of calling recall_score, precision_score, and fbeta_score inside the loop.
♻️ Example: bound the candidate grid
     # 고정 간격 후보와 실제 확률값을 함께 사용
+    MAX_PROBABILITY_CANDIDATES = 200
+
+    probability_candidates = np.quantile(
+        probabilities,
+        np.linspace(
+            0.0,
+            1.0,
+            MAX_PROBABILITY_CANDIDATES,
+        ),
+    )
+
     candidates = np.unique(
         np.concatenate(
             [
                 np.linspace(
                     0.0,
                     1.0,
                     101,
                 ),
-                probabilities,
+                probability_candidates,
             ]
         )
     )
🤖 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/hybrid_thresholds.py` around lines 185 - 236,
Reduce the threshold-search cost in the candidate generation and nested loops by
bounding observed probability candidates to a fixed-size quantile grid (for
example, at most 200 evenly spaced quantiles) while retaining the fixed 0.0–1.0
boundaries and uniqueness. Preserve valid normal_max/phishing_min ordering and
the existing metric-based selection behavior.
tests/analysis/test_execution.py (1)

35-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding the remaining hybrid fields to the builder.

_text_analysis omits decision_source, routing_reason, fallback_applied, and self_model.confidence. The fixtures in tests/infrastructure/rabbitmq/test_result_factory.py and tests/infrastructure/rabbitmq/test_consumer.py include them. classify_execution does not read those keys today, so no test fails. Adding them keeps one shared shape for the hybrid text response and protects the classifier tests if the classifier later reads routing metadata.

🤖 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_execution.py` around lines 35 - 56, Extend the
_text_analysis test-data builder to include decision_source, routing_reason, and
fallback_applied in the result payload, plus confidence under self_model, using
the shared hybrid response values and defaults established by the RabbitMQ
fixtures. Preserve the existing score, error, and Gemini-related fields while
keeping the returned schema consistent for classifier tests.
tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py (1)

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

Add coverage for the unreachable-target branch.

The parametrized cases cover the ValueError paths in _validate_inputs. The RuntimeError at data_science/SMSModel/modeling/hybrid_thresholds.py lines 310-314 has no test. That branch decides whether an unreachable recall target stops the workflow or silently returns a weak policy. Add a case where no threshold pair reaches target_recall, for example labels that Gemini and stacking both misclassify.

The label-set check at lines 121-130 is also untested. A single-label array is a cheap case to add.

🤖 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/modeling/test_hybrid_thresholds.py` around lines
105 - 152, Add parametrized coverage in test_rejects_invalid_inputs for a
single-label labels array to exercise the label-set validation, and add a
separate test for select_hybrid_thresholds where Gemini and stacking both
misclassify so no threshold pair reaches target_recall and the
unreachable-target RuntimeError is asserted. Use the existing test helpers and
invocation style.
tests/infrastructure/rabbitmq/test_result_factory.py (1)

144-153: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for the fallback and unavailable methods.

These assertions cover the STACKING_GEMINI path only. TextAnalysisMethod in app/infrastructure/rabbitmq/schemas.py also defines STACKING, STACKING_FALLBACK, and UNAVAILABLE. The PR objectives describe Gemini timeouts, API errors, and rate limits that fall back to Stacking, and an ALL_TEXT_ENGINES_UNAVAILABLE outcome. The mapping from text-analysis metadata to those three enum values is untested here, and it is the metadata that downstream consumers read.

Add cases for a fallback result (fallback_applied: True) and for an all-engines-unavailable result.

🤖 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/infrastructure/rabbitmq/test_result_factory.py` around lines 144 - 153,
Add test cases alongside the existing STACKING_GEMINI assertions to cover
metadata mapping for a fallback result with fallback_applied=True, expecting
TextAnalysisMethod.STACKING_FALLBACK, and an all-engines-unavailable result,
expecting TextAnalysisMethod.UNAVAILABLE and the corresponding outcome metadata.
Reuse the existing event factory and assertion style so downstream-readable
fields are verified for both paths.
tests/analysis/text/test_hybrid_analyzer.py (1)

171-192: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the upstream exception text does not leak into the result.

The test uses the message "secret upstream detail" to model sensitive upstream detail. It does not verify that the message is absent from the returned payload. Add an explicit assertion so a future change that surfaces str(exception) fails this test.

💚 Proposed addition
     assert (
         result["gemini"]["error_message"]
         == "GEMINI_ANALYZER_FAILED"
     )
+    assert "secret upstream detail" not in str(result)
🤖 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_analyzer.py` around lines 171 - 192, Update
test_uses_stacking_when_gemini_raises to assert that the returned result
contains no occurrence of the sensitive text "secret upstream detail",
especially within result["gemini"]["error_message"]. Preserve the existing
fallback and sanitized error-code assertions.
app/analysis/text/hybrid_analyzer.py (2)

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

Silence the BLE001 warnings for the intentional fail-safe catches.

Both broad catches are deliberate. They normalize engine failures instead of propagating them. app/analysis/text/stacking_analyzer.py already marks the same pattern with # noqa: BLE001. Apply the same marker here for consistency with the linter configuration.

♻️ Proposed change
-        except Exception as exception:
+        except Exception as exception:  # noqa: BLE001

Also applies to: 172-172

🤖 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` at line 106, Add the # noqa: BLE001
marker to both intentional broad Exception handlers in the relevant analyzer
methods, including the catches around the lines represented by “except Exception
as exception.” Match the existing suppression pattern used in
stacking_analyzer.py without changing the fail-safe behavior.

Source: Linters/SAST tools


269-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use routing.decision.value instead of the hardcoded enum.

This branch runs only when stacking_analysis["is_available"] is falsy. ConditionalGeminiPolicy.route already returns GEMINI_FALLBACK for that input, so the hardcoded value duplicates the policy decision. The adjacent routing_reason already reads from routing. Reading both fields from routing keeps the metadata consistent if the policy adds a new unavailable-model decision later.

♻️ Proposed change
-            "routing_decision": (
-                HybridRoutingDecision
-                .GEMINI_FALLBACK
-                .value
-            ),
+            "routing_decision": (
+                routing.decision.value
+            ),

Remove the now-unused HybridRoutingDecision import if no other reference remains.

🤖 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 269 - 273, Update the
unavailable-model branch in the hybrid analysis flow to set routing_decision
from routing.decision.value, matching the adjacent routing_reason metadata and
preserving the policy’s returned decision. Remove the HybridRoutingDecision
import if no other references remain.
tests/analysis/test_hybrid_policy.py (1)

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

Add tests for the two uncovered guard branches and the threshold boundaries.

ConditionalGeminiPolicy.route has two fail-safe branches that no test exercises: STACKING_PROBABILITY_UNAVAILABLE (line 92 of app/analysis/hybrid_policy.py) and INVALID_STACKING_PROBABILITY (line 103). Both prevent a malformed stacking result from being treated as normal. The boundary values normal_max and phishing_min are also untested, and the comparisons use <= and >=.

💚 Proposed additional tests
`@pytest.mark.parametrize`(
    "probability",
    [None, "0.5", float("nan"), 1.5, -0.1],
)
def test_falls_back_to_gemini_for_unusable_probability(
    policy: ConditionalGeminiPolicy,
    probability,
) -> None:
    result = policy.route(
        {
            "engine": "stacking",
            "is_available": True,
            "result": {"risk_probability": probability},
        }
    )

    assert (
        result.decision
        == HybridRoutingDecision.GEMINI_FALLBACK
    )
    assert result.should_call_gemini is True


`@pytest.mark.parametrize`(
    ("probability", "expected"),
    [
        (0.2, HybridRoutingDecision.SELF_MODEL_NORMAL),
        (0.8, HybridRoutingDecision.SELF_MODEL_PHISHING),
    ],
)
def test_threshold_boundaries_are_inclusive(
    policy: ConditionalGeminiPolicy,
    probability: float,
    expected: HybridRoutingDecision,
) -> None:
    result = policy.route(
        build_stacking_result(probability)
    )

    assert result.decision == expected
    assert result.should_call_gemini is False
🤖 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_hybrid_policy.py` around lines 82 - 119, Add tests in
tests/analysis/test_hybrid_policy.py covering ConditionalGeminiPolicy.route’s
unavailable and invalid stacking-probability fallback branches with None,
nonnumeric, NaN, and out-of-range values, asserting GEMINI_FALLBACK and
should_call_gemini=True. Also add boundary tests for HybridThresholds using
probabilities equal to normal_max and phishing_min, asserting the inclusive
SELF_MODEL_NORMAL and SELF_MODEL_PHISHING decisions without Gemini.
tests/analysis/test_service.py (1)

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

Assert the exact final score for the Gemini escalation path.

assert result.final_score >= 40 passes for a wide range of values. It does not distinguish the Gemini score of 90 from the blended text-track score that RiskScoringEngine currently computes from self_model_score=50 and selected_score=90. See the related comment on app/analysis/service.py Lines 331-336.

Assert the exact expected score and the text contribution. A precise assertion documents which score the pipeline applies and fails if the blending semantics change.

💚 Proposed change
     assert result.status == "SUCCESS"
     assert result.text_analysis["decision_source"] == "GEMINI"
     assert result.text_analysis["gemini_available"] is True
-    assert result.final_score >= 40
+    # 텍스트 트랙이 어떤 점수를 적용했는지 명시적으로 고정
+    assert result.contribution_breakdown.llm == <expected>
+    assert result.final_score == <expected>
🤖 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 120 - 123, Update the Gemini
escalation assertions in the test to verify the exact expected final_score
produced by the current blending logic, and assert the corresponding text
contribution as described by RiskScoringEngine. Replace the broad lower-bound
check while preserving the existing status, decision_source, and
gemini_available assertions.
🤖 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/service.py`:
- Around line 331-336: The service text-track call at app/analysis/service.py
lines 331-336 should pass only the HybridTextAnalyzer-selected score as the
single text signal, remove the raw naive_bayes_score=self_model_score argument,
and derive llm_available from whether a selected score exists. Update
tests/analysis/test_service.py lines 120-123 to assert the exact
result.final_score and result.contribution_breakdown.llm values, pinning the
applied selected text score.
- Around line 136-146: Update the asyncio.gather flow around text_task and
url_task to collect both task outcomes and handle exceptions explicitly. When
either task fails, cancel the sibling task, await its completion, and preserve
the existing fail-safe ERROR response; ensure both task results or exceptions
are retrieved so no background work or unhandled task warning remains.

In `@app/analysis/text/hybrid_analyzer.py`:
- Around line 104-105: Update the async analyze flow around
self.stacking_analyzer in HybridAnalyzer.analyze to execute the synchronous
callable via asyncio.to_thread, adding the asyncio import. Preserve the existing
arguments, result handling, and exception behavior while preventing blocking of
the event loop.

In `@app/core/config.py`:
- Around line 20-32: Update the default values for
STACKING_NORMAL_PROBABILITY_MAX and STACKING_PHISHING_PROBABILITY_MIN to
conservative, non-degenerate thresholds such as 0.1 and 0.9, while preserving
their existing 0–1 validation. Ensure the defaults no longer make nearly every
probability fall into the uncertain band.

In `@app/infrastructure/rabbitmq/result_factory.py`:
- Around line 163-165: Update the result mapping around selfModelConfidence and
add a _confidence helper next to _integer_score. Have the helper reject
booleans, non-numeric values, and NaN as None, while converting valid numbers to
float and clamping finite values to the 0.0–1.0 range before assigning
selfModelConfidence.

In `@data_science/SMSModel/artifacts/stacking/gemini_validation_predictions.json`:
- Around line 1-108: Document that the committed gemini_validation_predictions
cache is intentionally partial because of Gemini quota limits, including that
unavailable entries prevent threshold selection and are not a regression. Add
this note to the relevant PR or documentation while leaving the
fingerprint-and-score cache unchanged.

In `@data_science/SMSModel/run_hybrid_threshold_selection.py`:
- Around line 351-370: Update the score validation in the available calculation
to explicitly reject bool values while continuing to accept only integer scores
in the existing 0–100 range; preserve the current integer risk_score contract
and downstream _ordered_gemini_scores behavior.

In `@docs/PII_MASKING.md`:
- Around line 24-26: Update the deployment sequence in the SafeFam_BE/SafeFam_AI
token-format migration instructions to deploy SafeFam_AI before SafeFam_BE.
Alternatively, require both token formats to be supported by SafeFam_AI before
deploying the new-emitting SafeFam_BE version.

In `@docs/SCORING_PIPELINE_CHANGES.md`:
- Around line 109-120: Update docs/SCORING_PIPELINE_CHANGES.md lines 109-120 to
distinguish final-grade thresholds (0–39 LOW, 40–69 MEDIUM, 70–100 HIGH) from
the separate thresholds that route uncertain results to Gemini, documenting the
selected hybrid policy without conflating the two. Update docs/TRAINING_FLOW.md
lines 74-82 to include Recall, F2, Gemini call rate, fingerprint caching, policy
metadata, and the provisional validation state.
- Around line 123-136: Update the “테스트 현황” section to report the complete
validation result, including 434 passed and 3 deselected tests, the exact test
scope, and the Gemini quota limitation. Mark the final thresholds and Gemini
call rate as provisional until those limitations are resolved, while retaining
the existing list of relevant tests and out-of-scope files.
- Around line 5-17: Update the documentation to match the current
Stacking/Gemini contract: in docs/SCORING_PIPELINE_CHANGES.md lines 5-17,
replace the Naive Bayes-first and SAFE-only routing diagram with high-confidence
and uncertain-result routing; in docs/PII_MASKING.md lines 18-20, identify the
Stacking hybrid analyzer and Gemini uncertainty path; and in
docs/TRAINING_FLOW.md line 172, replace Claude and medium-only escalation
terminology with Gemini and the current routing policy.
- Around line 102-105: Update the scoring documentation around the VirusTotal
formula to state that raw_score is normalized to 0–1, while
app/infrastructure/rabbitmq/result_factory.py:_url_score converts it to 0–100
for external output. Add boundary examples for raw scores 0 and 1.0, plus the
zero-engines case, and clarify that callers must not apply the conversion twice.

In `@docs/TRAINING_FLOW.md`:
- Around line 113-118: The documentation for generate_voice_data.py must clarify
that its precomputed 68/16/16 split assignment is informational because
_leak_free_split() reassigns splits; alternatively remove the precomputed split
claim. Explicitly state whether each 15% allocation is calculated from the
original dataset or from the remaining subset, consistently in both affected
sections.

---

Nitpick comments:
In `@app/analysis/text/hybrid_analyzer.py`:
- Line 106: Add the # noqa: BLE001 marker to both intentional broad Exception
handlers in the relevant analyzer methods, including the catches around the
lines represented by “except Exception as exception.” Match the existing
suppression pattern used in stacking_analyzer.py without changing the fail-safe
behavior.
- Around line 269-273: Update the unavailable-model branch in the hybrid
analysis flow to set routing_decision from routing.decision.value, matching the
adjacent routing_reason metadata and preserving the policy’s returned decision.
Remove the HybridRoutingDecision import if no other references remain.

In `@data_science/SMSModel/modeling/hybrid_thresholds.py`:
- Around line 185-236: Reduce the threshold-search cost in the candidate
generation and nested loops by bounding observed probability candidates to a
fixed-size quantile grid (for example, at most 200 evenly spaced quantiles)
while retaining the fixed 0.0–1.0 boundaries and uniqueness. Preserve valid
normal_max/phishing_min ordering and the existing metric-based selection
behavior.

In `@data_science/SMSModel/run_hybrid_threshold_selection.py`:
- Around line 101-176: The checksum validation in _load_stacking_classifier only
detects corruption or stale artifacts, not tampering, because model_sha256 is
read from the colocated metadata.json and joblib.load can execute pickle code.
Add a short comment immediately before the checksum comparison or
deserialization documenting this trust boundary and explicitly stating that the
check is not a security control.
- Around line 441-464: Define a single module-level constant for the Gemini
phishing score, then replace the hardcoded 40 in _write_policy_report and the
other usage near line 527, and use the same constant as the default
gemini_phishing_score in select_hybrid_thresholds so selection and reporting
remain consistent.

In `@tests/analysis/test_execution.py`:
- Around line 35-56: Extend the _text_analysis test-data builder to include
decision_source, routing_reason, and fallback_applied in the result payload,
plus confidence under self_model, using the shared hybrid response values and
defaults established by the RabbitMQ fixtures. Preserve the existing score,
error, and Gemini-related fields while keeping the returned schema consistent
for classifier tests.

In `@tests/analysis/test_hybrid_policy.py`:
- Around line 82-119: Add tests in tests/analysis/test_hybrid_policy.py covering
ConditionalGeminiPolicy.route’s unavailable and invalid stacking-probability
fallback branches with None, nonnumeric, NaN, and out-of-range values, asserting
GEMINI_FALLBACK and should_call_gemini=True. Also add boundary tests for
HybridThresholds using probabilities equal to normal_max and phishing_min,
asserting the inclusive SELF_MODEL_NORMAL and SELF_MODEL_PHISHING decisions
without Gemini.

In `@tests/analysis/test_service.py`:
- Around line 120-123: Update the Gemini escalation assertions in the test to
verify the exact expected final_score produced by the current blending logic,
and assert the corresponding text contribution as described by
RiskScoringEngine. Replace the broad lower-bound check while preserving the
existing status, decision_source, and gemini_available assertions.

In `@tests/analysis/text/test_hybrid_analyzer.py`:
- Around line 171-192: Update test_uses_stacking_when_gemini_raises to assert
that the returned result contains no occurrence of the sensitive text "secret
upstream detail", especially within result["gemini"]["error_message"]. Preserve
the existing fallback and sanitized error-code assertions.

In `@tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py`:
- Around line 105-152: Add parametrized coverage in test_rejects_invalid_inputs
for a single-label labels array to exercise the label-set validation, and add a
separate test for select_hybrid_thresholds where Gemini and stacking both
misclassify so no threshold pair reaches target_recall and the
unreachable-target RuntimeError is asserted. Use the existing test helpers and
invocation style.

In `@tests/infrastructure/rabbitmq/test_result_factory.py`:
- Around line 144-153: Add test cases alongside the existing STACKING_GEMINI
assertions to cover metadata mapping for a fallback result with
fallback_applied=True, expecting TextAnalysisMethod.STACKING_FALLBACK, and an
all-engines-unavailable result, expecting TextAnalysisMethod.UNAVAILABLE and the
corresponding outcome metadata. Reuse the existing event factory and assertion
style so downstream-readable fields are verified for both paths.
🪄 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: 5bba9692-de3c-4670-bef0-161d9095a7e9

📥 Commits

Reviewing files that changed from the base of the PR and between dc6b9fd and dc78eeb.

📒 Files selected for processing (22)
  • app/analysis/execution.py
  • app/analysis/hybrid_policy.py
  • app/analysis/service.py
  • app/analysis/text/hybrid_analyzer.py
  • app/core/config.py
  • app/infrastructure/rabbitmq/result_factory.py
  • app/infrastructure/rabbitmq/schemas.py
  • data_science/SMSModel/artifacts/stacking/gemini_validation_predictions.json
  • data_science/SMSModel/modeling/hybrid_thresholds.py
  • data_science/SMSModel/run_hybrid_threshold_selection.py
  • docs/PII_MASKING.md
  • docs/SCORING_PIPELINE_CHANGES.md
  • docs/TRAINING_FLOW.md
  • tests/analysis/test_execution.py
  • tests/analysis/test_hybrid_policy.py
  • tests/analysis/test_router.py
  • tests/analysis/test_service.py
  • tests/analysis/text/test_hybrid_analyzer.py
  • tests/core/test_config.py
  • tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py
  • tests/infrastructure/rabbitmq/test_consumer.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
🛑 Comments failed to post (6)
docs/PII_MASKING.md (1)

24-26: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Deploy the consumer before the producer after token-format changes.

If SafeFam_BE emits a new token format before SafeFam_AI supports it, FastAPI can miss token-specific preprocessing and rules. Deploy SafeFam_AI first, or support both token formats before deploying SafeFam_BE.

Proposed documentation change
-3. Spring → FastAPI 순서로 배포
+3. FastAPI(SafeFam_AI) → Spring(SafeFam_BE) 순서로 배포
📝 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.

1. `SafeFam_BE`와 `SafeFam_AI` 양쪽 레포에서 토큰 형식 동시 수정
2. 양쪽 테스트 통과 확인
3. FastAPI(SafeFam_AI) → Spring(SafeFam_BE) 순서로 배포
🤖 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 `@docs/PII_MASKING.md` around lines 24 - 26, Update the deployment sequence in
the SafeFam_BE/SafeFam_AI token-format migration instructions to deploy
SafeFam_AI before SafeFam_BE. Alternatively, require both token formats to be
supported by SafeFam_AI before deploying the new-emitting SafeFam_BE version.
docs/SCORING_PIPELINE_CHANGES.md (4)

5-17: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the documentation with the current Stacking/Gemini contract.

  • docs/SCORING_PIPELINE_CHANGES.md#L5-L17: Replace the Naive Bayes-first diagram and SAFE-only bypass description with the current high-confidence and uncertain-result routing.
  • docs/PII_MASKING.md#L18-L20: Name the current Stacking hybrid analyzer and Gemini uncertainty route.
  • docs/TRAINING_FLOW.md#L172: Replace Claude terminology and medium-only escalation with Gemini and the current routing policy.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 3 files
  • docs/SCORING_PIPELINE_CHANGES.md#L5-L17 (this comment)
  • docs/PII_MASKING.md#L18-L20
  • docs/TRAINING_FLOW.md#L172-L172
🤖 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 `@docs/SCORING_PIPELINE_CHANGES.md` around lines 5 - 17, Update the
documentation to match the current Stacking/Gemini contract: in
docs/SCORING_PIPELINE_CHANGES.md lines 5-17, replace the Naive Bayes-first and
SAFE-only routing diagram with high-confidence and uncertain-result routing; in
docs/PII_MASKING.md lines 18-20, identify the Stacking hybrid analyzer and
Gemini uncertainty path; and in docs/TRAINING_FLOW.md line 172, replace Claude
and medium-only escalation terminology with Gemini and the current routing
policy.

102-105: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 'malicious|suspicious|ratio|score' \
  app/service/security/virustotal.py \
  app/service/security/hybrid_url_engine.py \
  app/utils/scoring_engine.py

Repository: SafeFam/SafeFam_AI

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg -i '(^|/)(virustotal|hybrid.*url|scoring.*engine|score|pipeline).*|SCORING_PIPELINE_CHANGES\.md$' || true

printf '%s\n' '--- relevant symbols ---'
rg -n -i -C 3 'malicious|suspicious|전체엔진수|VirusTotal|virustotal|hybrid_url_engine|ratio' . \
  -g '!node_modules' -g '!dist' -g '!build' \
  | head -n 400

Repository: SafeFam/SafeFam_AI

Length of output: 26177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- raw score consumers ---'
rg -n -C 5 '\braw_score\b|\burl_risk_score\b|\bscore\b' app tests docs \
  -g '*.py' -g '*.md' | head -n 500

printf '%s\n' '--- implementation structure ---'
ast-grep outline app/infrastructure/virustotal/client.py
fd -i -t f 'scoring|hybrid|url' app tests docs | head -n 200

printf '%s\n' '--- focused source ---'
cat -n app/infrastructure/virustotal/client.py | sed -n '100,142p'

printf '%s\n' '--- documentation ---'
cat -n docs/SCORING_PIPELINE_CHANGES.md | sed -n '95,145p'

Repository: SafeFam/SafeFam_AI

Length of output: 37818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- URL analyzer scoring path ---'
cat -n app/analysis/url/analyzer.py | sed -n '120,185p'

printf '%s\n' '--- public score conversion ---'
rg -n -C 8 'def _url_score|def _integer_score|final_score|weighted_contributions|url_risk_score' \
  app/infrastructure/rabbitmq/result_factory.py app/analysis/scoring.py app/analysis/hybrid_policy.py

printf '%s\n' '--- focused tests ---'
cat -n tests/infrastructure/test_virustotal_client.py | sed -n '1,145p'
rg -n -C 5 'url_risk_score|_url_score|0\.0|100|boundary|score' tests app/infrastructure/rabbitmq \
  -g '*.py' | head -n 400

printf '%s\n' '--- exact formula and conversion probe ---'
python3 - <<'PY'
from decimal import Decimal, ROUND_HALF_UP

def vt_raw(malicious, suspicious, total):
    if total <= 0:
        return 0.0
    return round(min(malicious / total + (suspicious / total) * 0.5, 1.0), 2)

def public_score(value):
    # Candidate behavior to compare with the repository's eventual _url_score implementation.
    return round(value * 100)

for case in [(0, 0, 0), (1, 0, 90), (0, 1, 90), (10, 0, 100), (0, 10, 100), (100, 0, 100), (90, 20, 110)]:
    raw = vt_raw(*case)
    print(case, 'raw=', raw, 'public_if_scaled=', public_score(raw))
PY

Repository: SafeFam/SafeFam_AI

Length of output: 48805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- URL analyzer scoring path ---'
cat -n app/analysis/url/analyzer.py | sed -n '120,185p'

printf '%s\n' '--- public score conversion ---'
rg -n -C 8 'def _url_score|def _integer_score|final_score|weighted_contributions|url_risk_score' \
  app/infrastructure/rabbitmq/result_factory.py app/analysis/scoring.py app/analysis/hybrid_policy.py

printf '%s\n' '--- focused tests ---'
cat -n tests/infrastructure/test_virustotal_client.py | sed -n '1,145p'
rg -n -C 5 'url_risk_score|_url_score|0\.0|100|boundary|score' tests app/infrastructure/rabbitmq \
  -g '*.py' | head -n 400

printf '%s\n' '--- exact formula and conversion probe ---'
python3 - <<'PY'
def vt_raw(malicious, suspicious, total):
    if total <= 0:
        return 0.0
    return round(min(malicious / total + (suspicious / total) * 0.5, 1.0), 2)

def public_score(value):
    return round(value * 100)

for case in [(0, 0, 0), (1, 0, 90), (0, 1, 90), (10, 0, 100), (0, 10, 100), (100, 0, 100), (90, 20, 110)]:
    raw = vt_raw(*case)
    print(case, 'raw=', raw, 'public_if_scaled=', public_score(raw))
PY

Repository: SafeFam/SafeFam_AI

Length of output: 49111


Document the VirusTotal score units.

The formula produces a normalized raw_score in 0–1. State that app/infrastructure/rabbitmq/result_factory.py:_url_score converts it to 0–100 for external output. Add boundary examples for 0, 1.0, and zero engines to prevent double conversion.

🤖 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 `@docs/SCORING_PIPELINE_CHANGES.md` around lines 102 - 105, Update the scoring
documentation around the VirusTotal formula to state that raw_score is
normalized to 0–1, while
app/infrastructure/rabbitmq/result_factory.py:_url_score converts it to 0–100
for external output. Add boundary examples for raw scores 0 and 1.0, plus the
zero-engines case, and clarify that callers must not apply the conversion twice.

109-120: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the selected hybrid policy separately from model scoring.

  • docs/SCORING_PIPELINE_CHANGES.md#L109-L120: Identify which thresholds classify the final grade and which thresholds route uncertain results to Gemini.
  • docs/TRAINING_FLOW.md#L74-L82: Add Recall, F2, Gemini call rate, fingerprint caching, policy metadata, and the provisional validation state.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 111-111: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 2 files
  • docs/SCORING_PIPELINE_CHANGES.md#L109-L120 (this comment)
  • docs/TRAINING_FLOW.md#L74-L82
🤖 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 `@docs/SCORING_PIPELINE_CHANGES.md` around lines 109 - 120, Update
docs/SCORING_PIPELINE_CHANGES.md lines 109-120 to distinguish final-grade
thresholds (0–39 LOW, 40–69 MEDIUM, 70–100 HIGH) from the separate thresholds
that route uncertain results to Gemini, documenting the selected hybrid policy
without conflating the two. Update docs/TRAINING_FLOW.md lines 74-82 to include
Recall, F2, Gemini call rate, fingerprint caching, policy metadata, and the
provisional validation state.

123-136: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Publish the complete and current validation status.

This section reports 59 passing tests but omits the broader 434 passed, 3 deselected result and the Gemini quota limitation. The final thresholds and Gemini call rate remain undetermined. Document the exact test scope and mark the policy values as provisional.

🤖 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 `@docs/SCORING_PIPELINE_CHANGES.md` around lines 123 - 136, Update the “테스트 현황”
section to report the complete validation result, including 434 passed and 3
deselected tests, the exact test scope, and the Gemini quota limitation. Mark
the final thresholds and Gemini call rate as provisional until those limitations
are resolved, while retaining the existing list of relevant tests and
out-of-scope files.
docs/TRAINING_FLOW.md (1)

113-118: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve the Voice split contract.

generate_voice_data.py is documented as assigning 68/16/16 split metadata, but _leak_free_split() is documented as ignoring that metadata and re-splitting the data. State that the generated split is informational, or remove it. Also specify whether each 15% value is calculated from the original dataset or the remaining subset.

Also applies to: 129-135

🤖 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 `@docs/TRAINING_FLOW.md` around lines 113 - 118, The documentation for
generate_voice_data.py must clarify that its precomputed 68/16/16 split
assignment is informational because _leak_free_split() reassigns splits;
alternatively remove the precomputed split claim. Explicitly state whether each
15% allocation is calculated from the original dataset or from the remaining
subset, consistently in both affected sections.

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

🤖 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/hybrid_analyzer.py`:
- Around line 133-140: Update the force-gemini override in the hybrid analyzer
to replace the routing result whenever force_gemini is True, regardless of
routing.should_call_gemini, preserving decision GEMINI_REVIEW,
should_call_gemini=True, and reason RULE_RISK_ESCALATION. Add coverage for an
uncertain Stacking score with force_gemini=True.
🪄 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: 6c8a9d9f-0a42-444e-a880-a8e2587ac6d4

📥 Commits

Reviewing files that changed from the base of the PR and between dc78eeb and 4e8e737.

📒 Files selected for processing (4)
  • app/analysis/service.py
  • app/analysis/text/hybrid_analyzer.py
  • tests/analysis/test_service.py
  • tests/analysis/text/test_hybrid_analyzer.py

Comment thread app/analysis/text/hybrid_analyzer.py Outdated
@pearseona pearseona changed the title Feat(#37): Gemini 조건부 호출 및 하이브리드 텍스트 분석 파이프라인 구현 Feat(#37): Gemini 조건부 호출 및 하이브리드 텍스트 분석 파이프라인 구현 (2/3) Aug 11, 2026
@pearseona
pearseona merged commit 306b757 into develop Aug 11, 2026
3 checks passed
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