Skip to content

Refactor(#60): Gemini 분석기를 AWS Bedrock(Claude Haiku)로 교체 - #61

Merged
pearseona merged 10 commits into
developfrom
refactor/60-bedrock-claude
Aug 12, 2026
Merged

Refactor(#60): Gemini 분석기를 AWS Bedrock(Claude Haiku)로 교체#61
pearseona merged 10 commits into
developfrom
refactor/60-bedrock-claude

Conversation

@pearseona

@pearseona pearseona commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📝 개요

기존 Gemini 기반 스미싱 분석기와 채팅 서비스를 AWS Bedrock 기반 Anthropic Claude Haiku로 교체했습니다.

Stacking 모델이 확신하는 구간에서는 LLM 호출을 생략하고, 불확실한 구간에서만 Bedrock을 호출하는 기존 하이브리드 정책을 공급자 중립적인 구조로 변경했습니다.

LLM 호출 실패 시에는 Stacking 결과로 fallback하고, 모든 텍스트 분석 엔진이 실패한 경우 UNKNOWN을 반환하는 fail-safe 정책을 유지했습니다.

🔗 관련 이슈

🎯 주요 변경 사항

AWS Bedrock Runtime Converse API 클라이언트 구현

  • 표준 AWS Credential Provider Chain을 사용합니다.
  • AWS_REGION, AWS_PROFILE, BEDROCK_MODEL_ID 설정을 지원합니다.
  • timeout 및 AWS SDK 표준 재시도를 적용했습니다.
  • throttling, timeout, access denied 및 잘못된 응답을 공개 오류 코드로 정규화했습니다.

Gemini 스미싱 분석기를 공급자 중립적인 LLM 분석기로 교체

  • gemini_analyzer.py를 제거하고 llm_analyzer.py를 추가했습니다.
  • Claude 응답을 risk_score, grade, tone_analysis, evidence, reason, error_message 형식으로 검증·정규화합니다.
  • 원문 메시지, 전체 모델 응답 및 AWS 자격 증명을 로그에 기록하지 않습니다.
  • JSON 파싱 또는 스키마 검증 실패 시 UNKNOWN을 반환합니다.

Hybrid 라우팅 구조를 LLM 기준으로 일반화

  • ConditionalGeminiPolicyConditionalLlmPolicy
  • GEMINI_REVIEWLLM_REVIEW
  • GEMINI_FALLBACKLLM_FALLBACK
  • gemini_analyzerllm_analyzer
  • gemini_calledllm_called
  • gemini_availablellm_available
  • TEXT:GEMINITEXT:LLM
  • decision_source=GEMINIdecision_source=LLM

기존 SafeFam_BE와의 한시적인 하위 호환성 유지

  • gemini, gemini_called, gemini_available
  • RabbitMQ geminiCalled
  • deprecated GEMINI, STACKING_GEMINI Enum
  • 신규 코드에서는 llm* 필드를 표준으로 사용합니다.

RabbitMQ 분석 결과에 공급자 중립 필드를 추가

  • llmCalled
  • llmProvider
  • llmModel
  • LLM, STACKING_LLM 분석 방식 Enum
  • LLM 실패 트랙을 TEXT:LLM으로 발행합니다.

채팅 서비스를 Gemini에서 Bedrock으로 교체

  • Gemini API Key, 모델, URL 및 전용 role 변환을 제거했습니다.
  • 공통 get_llm_client()를 사용합니다.
  • Bedrock Converse의 user, assistant role을 그대로 전달합니다.

Validation 캐시를 공급자 중립 구조로 일반화

  • gemini_validation_predictions.jsonllm_validation_predictions.json
  • 캐시 스키마를 v2로 변경했습니다.
  • provider, model ID, region, prompt version을 저장하고 현재 설정과 비교합니다.
  • --collect-gemini--collect-llm
  • 기존 Gemini validation 캐시는 제거했습니다.

Benchmark 및 adversarial test 도구를 Bedrock 기반 LLM 호출로 변경

  • 삭제된 Gemini 모듈 참조를 제거했습니다.
  • 공통 LLM factory와 Bedrock 모델 설정을 사용합니다.

📸 사진

  • 없음

✅ PR 체크리스트

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

Summary by CodeRabbit

  • New Features

    • Added AWS Bedrock support for SMS analysis and chat generation.
    • Added configurable model, region, timeout, retry, token, temperature, and risk-threshold settings.
    • Analysis results now include LLM provider, model, availability, and invocation details.
    • Added resilient routing with fallback between automated analysis methods.
  • Improvements

    • Standardized LLM terminology across analysis, chat, benchmarking, and reporting.
    • Added validation for risk scores and model responses.
    • Preserved compatibility with selected legacy result fields.
  • Documentation

    • Updated setup guidance and deployment configuration examples for AWS Bedrock.

@pearseona pearseona self-assigned this Aug 12, 2026
@pearseona pearseona added the refactor Code changes that neither fix a bug nor add a feature label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces Gemini with a provider-neutral LLM layer backed by AWS Bedrock. It adds shared client contracts, structured analysis validation, hybrid routing updates, RabbitMQ compatibility fields, threshold-selection changes, configuration updates, and tests.

Changes

Provider-neutral LLM migration

Layer / File(s) Summary
Configuration and analysis contracts
.env.example, .env.prod.example, README.md, app/core/config.py, app/analysis/hybrid_policy.py, app/infrastructure/llm/types.py, app/infrastructure/rabbitmq/schemas.py, data_science/SMSModel/modeling/*, data_science/SMSModel/run_hybrid_threshold_selection.py
Gemini settings and terminology are replaced with LLM and AWS Bedrock settings. Shared generation contracts, routing names, threshold parameters, cache metadata, and RabbitMQ fields are updated.
Bedrock client and analysis flow
app/infrastructure/llm/*, app/analysis/text/*, app/analysis/service.py, app/analysis/execution.py, app/chat/service.py
Bedrock generation, client caching, structured response validation, provider errors, chat generation, hybrid routing, and fallback behavior are implemented.
Downstream metadata and tooling
app/infrastructure/rabbitmq/*, scripts/adversarial_test/*, scripts/benchmark/*, requirements.txt
Downstream result mapping, adversarial scripts, benchmarks, rate limiting, and the Bedrock SDK dependency now use the shared LLM integration.
Updated and new validation coverage
tests/analysis/*, tests/chat/*, tests/core/*, tests/data_science/*, tests/infrastructure/*
Tests cover Bedrock requests, factory caching, LLM analysis, chat behavior, hybrid routing, configuration, threshold selection, RabbitMQ metadata, and compatibility behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AnalysisService
  participant HybridTextAnalyzer
  participant LlmSmishingAnalyzer
  participant BedrockLlmClient
  AnalysisService->>HybridTextAnalyzer: analyze text with force_llm
  HybridTextAnalyzer->>LlmSmishingAnalyzer: request LLM review
  LlmSmishingAnalyzer->>BedrockLlmClient: generate prompt and messages
  BedrockLlmClient-->>LlmSmishingAnalyzer: return LlmGeneration
  LlmSmishingAnalyzer-->>HybridTextAnalyzer: return validated analysis
  HybridTextAnalyzer-->>AnalysisService: return routed analysis result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing the Gemini analyzer with AWS Bedrock Claude Haiku.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/60-bedrock-claude

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

🧹 Nitpick comments (8)
tests/infrastructure/rabbitmq/test_result_factory.py (1)

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

Isolate the legacy alias path.

Line 257 leaves llm_available=True from _result(). An incorrect mapper that derives llmCalled from availability can pass this test without reading gemini_called. Set llm_available to False before creating the event.

Proposed fix
     assert result.text_analysis is not None
     result.text_analysis.pop("llm_called")
+    result.text_analysis["llm_available"] = False
     result.text_analysis["gemini_called"] = 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 `@tests/infrastructure/rabbitmq/test_result_factory.py` around lines 252 - 271,
Update test_factory_reads_legacy_gemini_called_alias to set
result.text_analysis["llm_available"] to False after removing llm_called and
setting gemini_called. Keep the assertion focused on verifying that the factory
reads the legacy gemini_called alias rather than deriving llmCalled from
availability.
tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py (1)

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

Use provider-neutral terminology.

Line 100 says “Gemini 호출”. The test validates LLM routing. Rename the comment to use “LLM” terminology.

Proposed fix
-    # Stacking만으로 완벽히 분류할 수 있으므로 Gemini 호출이 필요하지 않아야 함
+    # Stacking만으로 완벽히 분류할 수 있으므로 LLM 호출이 필요하지 않아야 함
🤖 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` at line 100,
Update the comment near the hybrid-threshold test to replace the
provider-specific “Gemini 호출” wording with provider-neutral “LLM” terminology,
while preserving its meaning that stacking alone should avoid an LLM call.
app/core/config.py (2)

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

Fix the comment typo.

ReabbitMQ should read RabbitMQ.

🤖 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/core/config.py` at line 93, Correct the comment typo by changing
“ReabbitMQ” to “RabbitMQ” in the execution settings comment.

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

Fold the blank checks into required_values.

Lines 125-133 check AWS_REGION and BEDROCK_MODEL_ID for blank values. Lines 135-137 add the same two fields to required_values, and line 147 applies an equivalent blank check. Keep one mechanism so that all missing production settings report in a single aggregated error message.

♻️ Proposed refactor
-        if not self.AWS_REGION.strip():
-            raise ValueError(
-                "AWS_REGION must not be blank"
-            )
-
-        if not self.BEDROCK_MODEL_ID.strip():
-            raise ValueError(
-                "BEDROCK_MODEL_ID must not be blank"
-            )
-
         required_values = {
🤖 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/core/config.py` around lines 125 - 141, Remove the separate blank-value
checks for AWS_REGION and BEDROCK_MODEL_ID, and rely on the existing
required_values validation to process them with the other production settings.
Preserve the aggregated error reporting so all missing or blank settings are
reported together.
app/infrastructure/llm/__init__.py (1)

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

Fix the docstring typo.

ingreastructure should read infrastructure.

📝 Proposed fix
-"""Provider-neutral LLM ingreastructure."""
+"""Provider-neutral LLM infrastructure."""
🤖 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/infrastructure/llm/__init__.py` at line 1, Correct the module docstring
in app/infrastructure/llm/__init__.py by changing the misspelled
“ingreastructure” to “infrastructure.”
app/analysis/text/llm_analyzer.py (3)

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

Suppress BLE001 instead of narrowing the catch.

Ruff flags the blind except Exception. The catch is correct here, because the analyzer must degrade to a fallback result rather than fail the whole analysis. If Ruff gates CI, add an explicit suppression with the reason.

🔧 Proposed fix
-    except Exception as exception:
+    except Exception as exception:  # noqa: BLE001 - degrade to a fallback 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 `@app/analysis/text/llm_analyzer.py` at line 216, Update the exception handler
in the analyzer flow around `except Exception as exception` to retain the broad
catch and add an explicit Ruff BLE001 suppression with a concise reason that the
analyzer must return its fallback result instead of failing the overall
analysis.

Source: Linters/SAST tools


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

Provider-neutral modules import a provider-specific exception. LlmProviderError is defined in app/infrastructure/llm/bedrock_client.py, so both consumers depend on the Bedrock implementation module to catch a normalized error. Define the exception in app/infrastructure/llm/types.py next to LlmGeneration and LlmClient, re-export it from bedrock_client.py for compatibility, then update both imports.

  • app/analysis/text/llm_analyzer.py#L13: import LlmProviderError from app.infrastructure.llm.types.
  • app/chat/service.py#L9: import LlmProviderError from app.infrastructure.llm.types.
🤖 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/llm_analyzer.py` at line 13, Move LlmProviderError into
app/infrastructure/llm/types.py alongside LlmGeneration and LlmClient, and
re-export it from bedrock_client.py to preserve compatibility. Update the
imports in app/analysis/text/llm_analyzer.py:13 and app/chat/service.py:9 to use
app.infrastructure.llm.types instead of the Bedrock module.

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

Remove the hardcoded Bedrock identifiers from the provider-neutral analyzer.

_failure_result defaults provider to "AWS_BEDROCK" and model_id to settings.BEDROCK_MODEL_ID. Lines 146-147 repeat the same defaults. The module docstring declares the analyzer provider-neutral, so these values contradict it and report the wrong provider once a second provider exists.

Report None when the client is unknown, and let the caller treat a missing provider as unattributed.

♻️ Proposed refactor
 def _failure_result(
     error_code: str,
     *,
-    provider: str = "AWS_BEDROCK",
+    provider: str | None = None,
     model_id: str | None = None,
 ) -> dict[str, Any]:
     return {
         "is_mock": False,
         "provider": provider,
-        "model_id": model_id or settings.BEDROCK_MODEL_ID,
+        "model_id": model_id,
-    provider = getattr(llm_client, "provider", "AWS_BEDROCK")
-    model_id = getattr(llm_client, "model_id", settings.BEDROCK_MODEL_ID)
+    provider = getattr(llm_client, "provider", None)
+    model_id = getattr(llm_client, "model_id", None)

Confirm that app/infrastructure/rabbitmq/result_factory.py tolerates a None llm_provider; TextAnalysisDetail.llmProvider is already str | None.

Also applies to: 146-147

🤖 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/llm_analyzer.py` around lines 96 - 102, Remove the AWS
Bedrock defaults from the provider-neutral _failure_result function and its
repeated caller values around the analysis failure path. Preserve explicit
provider and model identifiers when available, but pass or return None for
unknown clients so missing providers remain unattributed; verify result_factory
handling continues to accept a None llm_provider.
🤖 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/hybrid_policy.py`:
- Around line 51-69: In app/analysis/hybrid_policy.py#L51-L69, validate
result["risk_score"] before accepting the stacking result: route to LLM_FALLBACK
when it is Boolean, non-integer, below 0, or above 100. In
app/analysis/text/hybrid_analyzer.py#L28-L45, update _stacking_to_public_result
to reject risk_score values below 0 or above 100 while preserving valid integer
scores.

In `@app/core/config.py`:
- Around line 32-34: Confirm the target account’s Bedrock inference profile with
aws bedrock list-inference-profiles --region us-east-1, then update the
BEDROCK_MODEL_ID default in app/core/config.py#L32-L34 to the required regional
profile identifier, using the us. prefix if applicable. Update the sample values
at .env.example#L6 and .env.prod.example#L6 to match the corrected identifier,
selecting the production prefix for its deployment geography.
- Around line 86-87: Update the Settings declarations by removing the duplicate
RABBITMQ_ANALYSIS_REQUEST_ROUTING_KEY entry and adding
RABBITMQ_ANALYSIS_FAILED_ROUTING_KEY with the intended failed-analysis routing
value, matching the setting consumed by publisher logic.

In `@app/infrastructure/llm/bedrock_client.py`:
- Around line 59-73: Update the Bedrock client’s __init__ and generate flow to
enforce an end-to-end deadline: compute and store self._total_timeout_seconds
from settings.LLM_TIMEOUT_SECONDS and settings.LLM_MAX_RETRIES, wrap the
asyncio.to_thread request in asyncio.wait_for using that budget, and map
asyncio.TimeoutError to LLM_TIMEOUT.
- Around line 160-194: Update BedrockClient._convert_messages to validate that
messages begin with user and strictly alternate between user and assistant
before constructing the converted list. Raise the existing request-validation
exception for invalid sequences so client errors do not reach Converse or become
provider errors, while preserving the current role and content validation.

In `@scripts/adversarial_test/mutations.py`:
- Around line 145-150: Update _extract_text to reject provider-neutral English
refusal phrases, including “I can't help with that,” alongside the existing
_REFUSAL_MARKERS before returning text. Preserve the current empty-response
validation and ensure refusal text is never accepted as a mutation.

In `@scripts/benchmark/measure_llm_timing.py`:
- Line 47: Reject responses where result["is_mock"] is true before recording
benchmark data: in scripts/benchmark/measure_llm_timing.py at lines 47-47,
record the returned model_id instead of settings.BEDROCK_MODEL_ID; in
scripts/benchmark/run_llm_track.py at lines 52-52, reject mock results before
appending rows and use the returned model_id; at lines 79-79, log the validated
model identifier or explicitly label the value as the configured model.

In `@tests/data_science/SMSModel/test_hybrid_threshold_selection.py`:
- Around line 38-58: Update test_rejects_cache_runtime_mismatch to derive each
mismatched value from the configured runtime rather than using fixed literals:
append "-mismatch" to the configured model ID for model_id and to the configured
region for region, while preserving the existing parametrized validation and
error assertions.

In `@tests/infrastructure/llm/test_bedrock_client.py`:
- Line 119: Update the pytest.raises assertion’s match argument to use a raw
regex literal, changing the anchored pattern in the relevant test to the
raw-string form while preserving the existing pattern and expected
LlmProviderError behavior.

---

Nitpick comments:
In `@app/analysis/text/llm_analyzer.py`:
- Line 216: Update the exception handler in the analyzer flow around `except
Exception as exception` to retain the broad catch and add an explicit Ruff
BLE001 suppression with a concise reason that the analyzer must return its
fallback result instead of failing the overall analysis.
- Line 13: Move LlmProviderError into app/infrastructure/llm/types.py alongside
LlmGeneration and LlmClient, and re-export it from bedrock_client.py to preserve
compatibility. Update the imports in app/analysis/text/llm_analyzer.py:13 and
app/chat/service.py:9 to use app.infrastructure.llm.types instead of the Bedrock
module.
- Around line 96-102: Remove the AWS Bedrock defaults from the provider-neutral
_failure_result function and its repeated caller values around the analysis
failure path. Preserve explicit provider and model identifiers when available,
but pass or return None for unknown clients so missing providers remain
unattributed; verify result_factory handling continues to accept a None
llm_provider.

In `@app/core/config.py`:
- Line 93: Correct the comment typo by changing “ReabbitMQ” to “RabbitMQ” in the
execution settings comment.
- Around line 125-141: Remove the separate blank-value checks for AWS_REGION and
BEDROCK_MODEL_ID, and rely on the existing required_values validation to process
them with the other production settings. Preserve the aggregated error reporting
so all missing or blank settings are reported together.

In `@app/infrastructure/llm/__init__.py`:
- Line 1: Correct the module docstring in app/infrastructure/llm/__init__.py by
changing the misspelled “ingreastructure” to “infrastructure.”

In `@tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py`:
- Line 100: Update the comment near the hybrid-threshold test to replace the
provider-specific “Gemini 호출” wording with provider-neutral “LLM” terminology,
while preserving its meaning that stacking alone should avoid an LLM call.

In `@tests/infrastructure/rabbitmq/test_result_factory.py`:
- Around line 252-271: Update test_factory_reads_legacy_gemini_called_alias to
set result.text_analysis["llm_available"] to False after removing llm_called and
setting gemini_called. Keep the assertion focused on verifying that the factory
reads the legacy gemini_called alias rather than deriving llmCalled from
availability.
🪄 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: 489a5427-e143-421e-b4d9-37aa841dce6a

📥 Commits

Reviewing files that changed from the base of the PR and between 306b757 and fbc42c6.

📒 Files selected for processing (50)
  • .env.example
  • .env.prod.example
  • README.md
  • app/analysis/execution.py
  • app/analysis/hybrid_policy.py
  • app/analysis/rules/analyzer.py
  • app/analysis/scoring.py
  • app/analysis/service.py
  • app/analysis/text/gemini_analyzer.py
  • app/analysis/text/hybrid_analyzer.py
  • app/analysis/text/llm_analyzer.py
  • app/chat/service.py
  • app/core/config.py
  • app/infrastructure/gemini/__init__.py
  • app/infrastructure/gemini/client.py
  • app/infrastructure/llm/__init__.py
  • app/infrastructure/llm/bedrock_client.py
  • app/infrastructure/llm/factory.py
  • app/infrastructure/llm/types.py
  • app/infrastructure/mock_provider.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
  • requirements.txt
  • scripts/adversarial_test/daily_batch.py
  • scripts/adversarial_test/evaluate.py
  • scripts/adversarial_test/mutations.py
  • scripts/adversarial_test/rate_limit.py
  • scripts/benchmark/generate_llm_proxy_prompt.py
  • scripts/benchmark/measure_llm_timing.py
  • scripts/benchmark/run_llm_track.py
  • 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_gemini_analyzer.py
  • tests/analysis/text/test_hybrid_analyzer.py
  • tests/analysis/text/test_llm_analyzer.py
  • tests/chat/test_router.py
  • tests/chat/test_service.py
  • tests/core/test_config.py
  • tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py
  • tests/data_science/SMSModel/test_hybrid_threshold_selection.py
  • tests/infrastructure/llm/__init__.py
  • tests/infrastructure/llm/test_bedrock_client.py
  • tests/infrastructure/llm/test_factory.py
  • tests/infrastructure/rabbitmq/test_consumer.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
💤 Files with no reviewable changes (6)
  • app/infrastructure/gemini/init.py
  • tests/core/test_config.py
  • data_science/SMSModel/artifacts/stacking/gemini_validation_predictions.json
  • app/infrastructure/gemini/client.py
  • tests/analysis/text/test_gemini_analyzer.py
  • app/analysis/text/gemini_analyzer.py

Comment thread app/analysis/hybrid_policy.py
Comment thread app/core/config.py
Comment thread app/core/config.py Outdated
Comment thread app/infrastructure/llm/bedrock_client.py
Comment thread app/infrastructure/llm/bedrock_client.py
Comment thread scripts/adversarial_test/mutations.py
Comment thread scripts/benchmark/measure_llm_timing.py Outdated
Comment thread tests/data_science/SMSModel/test_hybrid_threshold_selection.py
Comment thread tests/infrastructure/llm/test_bedrock_client.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/benchmark/run_llm_track.py (1)

39-45: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not require error_message for mock results.

If result["is_mock"] is true, line 45 can raise KeyError because a mock response does not need an error_message. Read the error message safely and log a separate mock reason.

Proposed fix
-        if result.get("is_mock") or result.get("result", {}).get(
-            "error_message"
-        ):
+        error_message = result.get("result", {}).get("error_message")
+        if result.get("is_mock") or error_message:
             logger.warning(
-                "[LlmTrack] API 오류 응답 제외 id=%s error=%s",
+                "[LlmTrack] 응답 제외 id=%s error=%s",
                 sample["id"],
-                result["result"]["error_message"],
+                error_message or "mock response",
             )
🤖 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 `@scripts/benchmark/run_llm_track.py` around lines 39 - 45, Update the warning
branch handling result exclusions to avoid directly indexing
result["result"]["error_message"] when result.get("is_mock") is true. Safely
retrieve the error message, and log a distinct mock reason for mock responses
while preserving the existing API error details for non-mock error responses.
🤖 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/infrastructure/llm/bedrock_client.py`:
- Around line 102-122: Update the Bedrock call in the generate flow around
self._client.converse so it runs on a dedicated, bounded executor with bounded
in-flight admission rather than asyncio.to_thread’s shared default executor.
Ensure admission capacity is released only after the worker future actually
completes, including when asyncio.wait_for returns a timeout, while preserving
the existing LLM_TIMEOUT behavior.

---

Outside diff comments:
In `@scripts/benchmark/run_llm_track.py`:
- Around line 39-45: Update the warning branch handling result exclusions to
avoid directly indexing result["result"]["error_message"] when
result.get("is_mock") is true. Safely retrieve the error message, and log a
distinct mock reason for mock responses while preserving the existing API error
details for non-mock error responses.
🪄 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: c6b287c6-efde-4721-b5b8-1c69a205179c

📥 Commits

Reviewing files that changed from the base of the PR and between 859c646 and 34f0298.

📒 Files selected for processing (18)
  • .env.example
  • .env.prod.example
  • app/analysis/hybrid_policy.py
  • app/analysis/text/hybrid_analyzer.py
  • app/analysis/text/llm_analyzer.py
  • app/chat/service.py
  • app/core/config.py
  • app/infrastructure/llm/__init__.py
  • app/infrastructure/llm/bedrock_client.py
  • app/infrastructure/llm/types.py
  • scripts/adversarial_test/mutations.py
  • scripts/benchmark/measure_llm_timing.py
  • scripts/benchmark/run_llm_track.py
  • tests/analysis/test_hybrid_policy.py
  • tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py
  • tests/data_science/SMSModel/test_hybrid_threshold_selection.py
  • tests/infrastructure/llm/test_bedrock_client.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • app/infrastructure/llm/init.py
  • app/infrastructure/llm/types.py
  • tests/infrastructure/rabbitmq/test_result_factory.py
  • scripts/adversarial_test/mutations.py
  • app/chat/service.py
  • tests/data_science/SMSModel/modeling/test_hybrid_thresholds.py
  • app/analysis/text/llm_analyzer.py
  • scripts/benchmark/measure_llm_timing.py
  • app/analysis/text/hybrid_analyzer.py

Comment on lines +102 to +122
response = await asyncio.wait_for(
asyncio.to_thread(
self._client.converse,
modelId=self.model_id,
system=[{"text": system_prompt}],
messages=bedrock_messages,
inferenceConfig={
"maxTokens": (
max_tokens
if max_tokens is not None
else settings.LLM_MAX_OUTPUT_TOKENS
),
"temperature": (
temperature
if temperature is not None
else settings.LLM_TEMPERATURE
),
},
),
timeout=self._total_timeout_seconds,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

According to the official Python asyncio documentation, does cancelling asyncio.wait_for around asyncio.to_thread stop the underlying thread function?

💡 Result:

According to the official Python asyncio documentation and the underlying implementation, cancelling an asyncio.wait_for task that wraps asyncio.to_thread does not stop the underlying thread [1][2][3]. When you cancel an asyncio task (such as the one returned by asyncio.wait_for), you are cancelling the coroutine that is managing the thread execution, not the thread itself [2][4]. Because Python provides no general mechanism to forcibly terminate an arbitrary thread from the outside, the thread will continue to run until the blocking function completes naturally [2][3][4]. Key points to understand: - Python threads are not interruptible by external mechanisms like asyncio cancellation [1][3][5]. - asyncio.to_thread is a wrapper around the event loop's run_in_executor method [2][6]. When the task is cancelled, the asyncio wrapper stops waiting for the thread to finish and raises a CancelledError in the event loop, but it cannot send a signal to the running thread to cease execution [2][3][4]. - If you need to stop a long-running function in a thread, you must design it to check for a cancellation signal, such as using a threading.Event object that the thread periodically checks during its execution [2]. Alternatively, using multiprocessing and a ProcessPoolExecutor may provide mechanisms to terminate child processes if true, external termination is required [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target file outline ---'
ast-grep outline app/infrastructure/llm/bedrock_client.py
printf '%s\n' '--- target implementation ---'
cat -n app/infrastructure/llm/bedrock_client.py | sed -n '1,180p'
printf '%s\n' '--- executor and timeout references ---'
rg -n -C 3 'to_thread|run_in_executor|wait_for|LLM_TIMEOUT|total_timeout|retry|Config|ThreadPoolExecutor' app tests 2>/dev/null || true
printf '%s\n' '--- project runtime and dependency declarations ---'
rg -n -C 2 'python_requires|requires-python|boto3|botocore|asyncio' pyproject.toml setup.cfg setup.py requirements*.txt 2>/dev/null || true

Repository: SafeFam/SafeFam_AI

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- settings relevant to Bedrock limits ---'
cat -n app/core/config.py | sed -n '35,80p'
printf '%s\n' '--- Bedrock client construction and call sites ---'
rg -n -C 4 'BedrockLlmClient|generate\(' app tests/infrastructure/llm tests/analysis/text
printf '%s\n' '--- existing Bedrock tests around timeout and concurrency ---'
cat -n tests/infrastructure/llm/test_bedrock_client.py | sed -n '1,190p'
printf '%s\n' '--- runtime cancellation probe ---'
python3 - <<'PY'
import asyncio
import threading
import time

started = threading.Event()
finished = threading.Event()

def blocking_call():
    started.set()
    time.sleep(0.20)
    finished.set()
    return "done"

async def main():
    task = asyncio.create_task(
        asyncio.wait_for(asyncio.to_thread(blocking_call), timeout=0.02)
    )
    await asyncio.to_thread(started.wait, 1)
    try:
        await task
    except TimeoutError:
        print("wait_for_result=TimeoutError")
    print(f"finished_immediately={finished.is_set()}")
    await asyncio.to_thread(finished.wait, 1)
    print(f"finished_later={finished.is_set()}")

asyncio.run(main())
PY

Repository: SafeFam/SafeFam_AI

Length of output: 24210


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import asyncio
import threading
import time

started = threading.Event()
finished = threading.Event()

def blocking_call():
    started.set()
    time.sleep(0.20)
    finished.set()
    return "done"

async def main():
    task = asyncio.create_task(
        asyncio.wait_for(asyncio.to_thread(blocking_call), timeout=0.02)
    )
    await asyncio.to_thread(started.wait, 1)
    try:
        await task
    except asyncio.TimeoutError:
        print("wait_for_result=TimeoutError")
    print(f"finished_immediately={finished.is_set()}")
    await asyncio.to_thread(finished.wait, 1)
    print(f"finished_later={finished.is_set()}")

asyncio.run(main())
PY

Repository: SafeFam/SafeFam_AI

Length of output: 232


🌐 Web query:

site:docs.python.org/3/library/asyncio-task.html asyncio.to_thread cancellation wait_for

💡 Result:

When using asyncio.to_thread with asyncio.wait_for, canceling the task associated with asyncio.to_thread will cancel the future returned by the call, but it cannot stop the underlying thread from executing [1]. In Python's asyncio, to_thread is designed to run blocking IO-bound functions in a separate thread to avoid blocking the event loop [1]. While the asyncio task waiting for the result can be cancelled—for example, via a timeout in asyncio.wait_for—this cancellation only affects the event loop's side of the operation [1]. The thread spawned by to_thread will continue to run until the blocking function completes, as Python does not provide a safe way to forcefully terminate a running thread from another thread [1]. In summary: 1. If asyncio.wait_for reaches its timeout, it will cancel the asyncio task waiting for to_thread, raising a TimeoutError [1]. 2. The event loop will stop waiting for the thread's result, but the thread itself will continue to execute the function until it finishes naturally [1]. 3. Resources held by that thread will not be released until the function completes, and any result or exception generated by the thread after the timeout will be ignored by the asyncio task that was already cancelled [1].

Citations:


Prevent timed-out Bedrock calls from occupying default executor workers.

asyncio.wait_for cancels the await, but it does not stop a running asyncio.to_thread worker. The synchronous converse call can continue through its retry budget after generate returns LLM_TIMEOUT.

During a Bedrock outage, repeated timeouts can occupy the shared default executor and delay unrelated asyncio.to_thread work. Use a dedicated bounded executor and bounded in-flight admission. Release capacity only when the worker future completes.

🤖 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/infrastructure/llm/bedrock_client.py` around lines 102 - 122, Update the
Bedrock call in the generate flow around self._client.converse so it runs on a
dedicated, bounded executor with bounded in-flight admission rather than
asyncio.to_thread’s shared default executor. Ensure admission capacity is
released only after the worker future actually completes, including when
asyncio.wait_for returns a timeout, while preserving the existing LLM_TIMEOUT
behavior.

@pearseona
pearseona merged commit 78d56c4 into develop Aug 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Code changes that neither fix a bug nor add a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant