From a661984096e2f5d351257a8fef68b0c6017197e2 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:02:06 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[refactor/#56]=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20enum=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/enums.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/core/enums.py diff --git a/app/core/enums.py b/app/core/enums.py new file mode 100644 index 0000000..1c4aa5b --- /dev/null +++ b/app/core/enums.py @@ -0,0 +1,55 @@ +from enum import StrEnum + + +class RunStatus(StrEnum): + QUEUED = "queued" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class TranscribeRunPhase(StrEnum): + QUEUED = "queued" + TRANSCRIBING = "transcribing" + TRANSCRIPT_READY = "transcript_ready" + SUMMARY_READY = "summary_ready" + APPLICATIONS_READY = "applications_ready" + FAILED = "failed" + + +class CommitAnalyzeRunPhase(StrEnum): + QUEUED = "queued" + SUMMARIZING = "summarizing" + SUMMARY_READY = "summary_ready" + EMBEDDING = "embedding" + EMBEDDING_READY = "embedding_ready" + FAILED = "failed" + + +class TimelineStep(StrEnum): + ISSUE = "이슈제기" + DISCUSSION = "대안논의" + AGREEMENT = "적용합의" + + +class MatchStatus(StrEnum): + APPLIED = "APPLIED" + PARTIAL = "PARTIAL" + UNAPPLIED = "UNAPPLIED" + + +class CommitChangeDirection(StrEnum): + ADD = "add" + REMOVE = "remove" + MODIFY = "modify" + MIGRATE = "migrate" + + +class CommitType(StrEnum): + FEAT = "feat" + FIX = "fix" + DOCS = "docs" + REFACTOR = "refactor" + TEST = "test" + BUILD = "build" + CHORE = "chore" From 3b5d268e53b54f5e34da260cb9acc1f616a57922 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:02:36 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[refactor/#56]=20=ED=9A=8C=EC=9D=98=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20run=20=EC=83=81=ED=83=9C=20enum=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/domains/pipeline/schemas.py | 17 ++-------- .../pipeline/services/analysis_runs.py | 31 +++++++++---------- app/domains/transcribe/router.py | 5 +-- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/app/domains/pipeline/schemas.py b/app/domains/pipeline/schemas.py index 6528eec..287bca3 100644 --- a/app/domains/pipeline/schemas.py +++ b/app/domains/pipeline/schemas.py @@ -1,20 +1,9 @@ -from typing import Literal - from pydantic import BaseModel, Field +from app.core.enums import RunStatus, TranscribeRunPhase from app.domains.meeting_analysis.schemas import MeetingAnalysisResult from app.domains.transcribe.schemas import TranscribeSegment -RunStatus = Literal["queued", "processing", "completed", "failed"] -RunPhase = Literal[ - "queued", - "transcribing", - "transcript_ready", - "summary_ready", - "applications_ready", - "failed", -] - class TranscribeAnalysisResponse(BaseModel): meeting_id: str | None = Field( @@ -37,7 +26,7 @@ class TranscribeAnalysisResponse(BaseModel): class TranscribeAnalysisRunAccepted(BaseModel): run_id: str = Field(description="비동기 실행 식별자") status: RunStatus = Field(description='실행 상태("queued" 고정으로 시작)') - phase: RunPhase = Field(description='실행 단계("queued" 고정으로 시작)') + phase: TranscribeRunPhase = Field(description='실행 단계("queued" 고정으로 시작)') meeting_id: str | None = Field(default=None, description="요청 meeting_id echo") project_id: str | None = Field(default=None, description="요청 project_id echo") @@ -45,7 +34,7 @@ class TranscribeAnalysisRunAccepted(BaseModel): class TranscribeAnalysisRunStatus(BaseModel): run_id: str = Field(description="비동기 실행 식별자") status: RunStatus = Field(description="queued/processing/completed/failed") - phase: RunPhase = Field( + phase: TranscribeRunPhase = Field( description=( "queued/transcribing/transcript_ready/summary_ready/" "applications_ready/failed. " diff --git a/app/domains/pipeline/services/analysis_runs.py b/app/domains/pipeline/services/analysis_runs.py index e8f62a4..38dae46 100644 --- a/app/domains/pipeline/services/analysis_runs.py +++ b/app/domains/pipeline/services/analysis_runs.py @@ -3,9 +3,8 @@ from datetime import UTC, datetime, timedelta from uuid import uuid4 +from app.core.enums import RunStatus, TranscribeRunPhase from app.domains.pipeline.schemas import ( - RunPhase, - RunStatus, TranscribeAnalysisResponse, TranscribeAnalysisRunAccepted, TranscribeAnalysisRunStatus, @@ -19,7 +18,7 @@ class _RunRecord: run_id: str status: RunStatus - phase: RunPhase + phase: TranscribeRunPhase meeting_id: str | None project_id: str | None submitted_at: datetime @@ -51,7 +50,7 @@ def _cleanup_runs_locked() -> None: expired_ids = [] for run_id, run in _runs.items(): # 진행 중 실행은 TTL 삭제 대상에서 제외 - if run.status in {"queued", "processing"}: + if run.status in {RunStatus.QUEUED, RunStatus.PROCESSING}: continue reference_time = run.finished_at or run.submitted_at if reference_time < expiry_cutoff: @@ -67,7 +66,7 @@ def _cleanup_runs_locked() -> None: ( item for item in _runs.items() - if item[1].status not in {"queued", "processing"} + if item[1].status not in {RunStatus.QUEUED, RunStatus.PROCESSING} ), key=lambda item: item[1].submitted_at, ) @@ -101,16 +100,16 @@ async def create_run( run_id = uuid4().hex _runs[run_id] = _RunRecord( run_id=run_id, - status="queued", - phase="queued", + status=RunStatus.QUEUED, + phase=TranscribeRunPhase.QUEUED, meeting_id=meeting_id, project_id=project_id, submitted_at=_utc_now(), ) return TranscribeAnalysisRunAccepted( run_id=run_id, - status="queued", - phase="queued", + status=RunStatus.QUEUED, + phase=TranscribeRunPhase.QUEUED, meeting_id=meeting_id, project_id=project_id, ) @@ -122,15 +121,15 @@ async def mark_run_processing(run_id: str) -> None: run = _runs.get(run_id) if not run: return - run.status = "processing" - run.phase = "transcribing" + run.status = RunStatus.PROCESSING + run.phase = TranscribeRunPhase.TRANSCRIBING run.started_at = _utc_now() run.error = None async def mark_run_phase( run_id: str, - phase: RunPhase, + phase: TranscribeRunPhase, result: TranscribeAnalysisResponse | None = None, ) -> None: # 단계별 중간 결과를 저장하며 phase 갱신 @@ -149,8 +148,8 @@ async def mark_run_completed(run_id: str, result: TranscribeAnalysisResponse) -> run = _runs.get(run_id) if not run: return - run.status = "completed" - run.phase = "applications_ready" + run.status = RunStatus.COMPLETED + run.phase = TranscribeRunPhase.APPLICATIONS_READY run.result = result.model_copy(deep=True) run.error = None run.finished_at = _utc_now() @@ -162,8 +161,8 @@ async def mark_run_failed(run_id: str, error: str) -> None: run = _runs.get(run_id) if not run: return - run.status = "failed" - run.phase = "failed" + run.status = RunStatus.FAILED + run.phase = TranscribeRunPhase.FAILED run.error = error run.finished_at = _utc_now() diff --git a/app/domains/transcribe/router.py b/app/domains/transcribe/router.py index ec45a85..928e079 100644 --- a/app/domains/transcribe/router.py +++ b/app/domains/transcribe/router.py @@ -11,6 +11,7 @@ UploadFile, ) +from app.core.enums import TranscribeRunPhase from app.core.errors import AppServiceError from app.core.responses import ApiErrorResponse, ApiResponse, ok_response from app.domains.meeting_analysis.schemas import MeetingAnalysisResult @@ -183,7 +184,7 @@ async def _run_transcribe_application_run( ) await mark_run_phase( run_id=run_id, - phase="transcript_ready", + phase=TranscribeRunPhase.TRANSCRIPT_READY, result=partial_result, ) @@ -191,7 +192,7 @@ async def _run_transcribe_application_run( partial_result.analysis_result.overall_analysis = overall_analysis await mark_run_phase( run_id=run_id, - phase="summary_ready", + phase=TranscribeRunPhase.SUMMARY_READY, result=partial_result, ) From afe1a297ca9915b27a033c3d1f9d9cdd44f4d276 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:03:17 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[refactor/#56]=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20run=20=EC=83=81=ED=83=9C=20enum=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/domains/commit/schemas.py | 20 +++---------- app/domains/commit/services/analyze_runs.py | 33 ++++++++++----------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/app/domains/commit/schemas.py b/app/domains/commit/schemas.py index dc995d0..b2a9ac6 100644 --- a/app/domains/commit/schemas.py +++ b/app/domains/commit/schemas.py @@ -1,16 +1,8 @@ -from typing import Annotated, Literal +from typing import Annotated from pydantic import BaseModel, Field -CommitAnalyzeRunStatusValue = Literal["queued", "processing", "completed", "failed"] -CommitAnalyzeRunPhase = Literal[ - "queued", - "summarizing", - "summary_ready", - "embedding", - "embedding_ready", - "failed", -] +from app.core.enums import CommitAnalyzeRunPhase, RunStatus class ChangedFile(BaseModel): @@ -60,9 +52,7 @@ class CommitAnalyzeRunResult(BaseModel): class CommitAnalyzeRunAccepted(BaseModel): run_id: str = Field(description="비동기 실행 식별자") - status: CommitAnalyzeRunStatusValue = Field( - description='실행 상태("queued" 고정으로 시작)' - ) + status: RunStatus = Field(description='실행 상태("queued" 고정으로 시작)') phase: CommitAnalyzeRunPhase = Field( description='실행 단계("queued" 고정으로 시작)' ) @@ -73,9 +63,7 @@ class CommitAnalyzeRunAccepted(BaseModel): class CommitAnalyzeRunStatus(BaseModel): run_id: str = Field(description="비동기 실행 식별자") - status: CommitAnalyzeRunStatusValue = Field( - description="queued/processing/completed/failed" - ) + status: RunStatus = Field(description="queued/processing/completed/failed") phase: CommitAnalyzeRunPhase = Field( description=( "queued/summarizing/summary_ready/embedding/embedding_ready/failed. " diff --git a/app/domains/commit/services/analyze_runs.py b/app/domains/commit/services/analyze_runs.py index 9f156a5..c13946d 100644 --- a/app/domains/commit/services/analyze_runs.py +++ b/app/domains/commit/services/analyze_runs.py @@ -4,14 +4,13 @@ from datetime import UTC, datetime, timedelta from uuid import uuid4 +from app.core.enums import CommitAnalyzeRunPhase, RunStatus from app.core.errors import AppServiceError from app.domains.commit.schemas import ( CommitAnalyzeRequest, CommitAnalyzeRunAccepted, - CommitAnalyzeRunPhase, CommitAnalyzeRunResult, CommitAnalyzeRunStatus, - CommitAnalyzeRunStatusValue, ) from app.domains.commit.services.diff_filter import filter_changed_files from app.domains.commit.services.summarize import ( @@ -27,7 +26,7 @@ @dataclass class _CommitAnalyzeRunRecord: run_id: str - status: CommitAnalyzeRunStatusValue + status: RunStatus phase: CommitAnalyzeRunPhase request: CommitAnalyzeRequest submitted_at: datetime @@ -59,7 +58,7 @@ def _cleanup_runs_locked() -> None: expired_ids = [] for run_id, run in _runs.items(): # 진행 중 실행은 TTL 삭제 대상에서 제외 - if run.status in {"queued", "processing"}: + if run.status in {RunStatus.QUEUED, RunStatus.PROCESSING}: continue reference_time = run.finished_at or run.submitted_at if reference_time < expiry_cutoff: @@ -75,7 +74,7 @@ def _cleanup_runs_locked() -> None: ( item for item in _runs.items() - if item[1].status not in {"queued", "processing"} + if item[1].status not in {RunStatus.QUEUED, RunStatus.PROCESSING} ), key=lambda item: item[1].submitted_at, ) @@ -109,15 +108,15 @@ async def create_commit_analyze_run( run_id = uuid4().hex _runs[run_id] = _CommitAnalyzeRunRecord( run_id=run_id, - status="queued", - phase="queued", + status=RunStatus.QUEUED, + phase=CommitAnalyzeRunPhase.QUEUED, request=request.model_copy(deep=True), submitted_at=_utc_now(), ) return CommitAnalyzeRunAccepted( run_id=run_id, - status="queued", - phase="queued", + status=RunStatus.QUEUED, + phase=CommitAnalyzeRunPhase.QUEUED, commit_id=request.commit_id, commit_hash=request.commit_hash, repository_id=request.repository_id, @@ -130,8 +129,8 @@ async def _mark_run_processing(run_id: str) -> None: run = _runs.get(run_id) if not run: return - run.status = "processing" - run.phase = "summarizing" + run.status = RunStatus.PROCESSING + run.phase = CommitAnalyzeRunPhase.SUMMARIZING run.started_at = _utc_now() run.error = None @@ -157,8 +156,8 @@ async def _mark_run_completed(run_id: str, result: CommitAnalyzeRunResult) -> No run = _runs.get(run_id) if not run: return - run.status = "completed" - run.phase = "embedding_ready" + run.status = RunStatus.COMPLETED + run.phase = CommitAnalyzeRunPhase.EMBEDDING_READY run.result = result.model_copy(deep=True) run.error = None run.finished_at = _utc_now() @@ -170,8 +169,8 @@ async def _mark_run_failed(run_id: str, error: str) -> None: run = _runs.get(run_id) if not run: return - run.status = "failed" - run.phase = "failed" + run.status = RunStatus.FAILED + run.phase = CommitAnalyzeRunPhase.FAILED run.error = error run.finished_at = _utc_now() @@ -214,13 +213,13 @@ async def run_commit_analyze_pipeline(run_id: str) -> None: ) await _mark_run_phase( run_id=run_id, - phase="summary_ready", + phase=CommitAnalyzeRunPhase.SUMMARY_READY, result=result, ) await _mark_run_phase( run_id=run_id, - phase="embedding", + phase=CommitAnalyzeRunPhase.EMBEDDING, result=result, ) await store_commit_embedding( From 2081b7c08add5724fab910733f3999dcb4c82f07 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:03:40 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[refactor/#56]=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=99=80=20=EC=BB=A4=EB=B0=8B=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20enum=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/matching_scoring.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/app/domains/meeting_analysis/services/matching_scoring.py b/app/domains/meeting_analysis/services/matching_scoring.py index f499cbb..17a42ab 100644 --- a/app/domains/meeting_analysis/services/matching_scoring.py +++ b/app/domains/meeting_analysis/services/matching_scoring.py @@ -2,10 +2,8 @@ import re from dataclasses import dataclass -from typing import Literal -MatchStatus = Literal["APPLIED", "PARTIAL", "UNAPPLIED"] -CommitType = Literal["feat", "fix", "docs", "refactor", "test", "build", "chore"] +from app.core.enums import CommitType, MatchStatus _TOKEN_PATTERN = re.compile(r"[A-Za-z][A-Za-z0-9._/-]{1,}|[가-힣]{2,}") _PATH_LIKE_PATTERN = re.compile(r"[A-Za-z0-9._/-]+") @@ -181,18 +179,18 @@ } _COMMIT_TYPE_ALIASES: dict[str, CommitType] = { - "feature": "feat", - "feat": "feat", - "fix": "fix", - "bugfix": "fix", - "docs": "docs", - "doc": "docs", - "documentation": "docs", - "refactor": "refactor", - "test": "test", - "tests": "test", - "build": "build", - "chore": "chore", + "feature": CommitType.FEAT, + "feat": CommitType.FEAT, + "fix": CommitType.FIX, + "bugfix": CommitType.FIX, + "docs": CommitType.DOCS, + "doc": CommitType.DOCS, + "documentation": CommitType.DOCS, + "refactor": CommitType.REFACTOR, + "test": CommitType.TEST, + "tests": CommitType.TEST, + "build": CommitType.BUILD, + "chore": CommitType.CHORE, } _DOCS_APPLICATION_WORDS = { @@ -335,15 +333,15 @@ def infer_application_commit_types(application_text: str) -> set[CommitType]: expected_types: set[CommitType] = set() if tokens & _DOCS_APPLICATION_WORDS: - expected_types.update({"docs", "chore"}) + expected_types.update({CommitType.DOCS, CommitType.CHORE}) if tokens & _FIX_APPLICATION_WORDS and ( not expected_types or tokens & _FIX_ACTION_WORDS ): - expected_types.add("fix") + expected_types.add(CommitType.FIX) if tokens & _REFACTOR_APPLICATION_WORDS: - expected_types.add("refactor") + expected_types.add(CommitType.REFACTOR) if tokens & _FEATURE_APPLICATION_WORDS: - expected_types.add("feat") + expected_types.add(CommitType.FEAT) return expected_types @@ -358,8 +356,8 @@ def score_commit_type_match( expected_types = infer_application_commit_types(application_text) commit_tokens = extract_text_tokens(f"{commit_message} {commit_text}") if ( - commit_type == "chore" - and "docs" in expected_types + commit_type == CommitType.CHORE + and CommitType.DOCS in expected_types and not commit_tokens & _DOCS_APPLICATION_WORDS ): return 0 @@ -498,10 +496,10 @@ def is_ambiguous_application( def resolve_match_status(total_score: int) -> MatchStatus: if total_score >= 70: - return "APPLIED" + return MatchStatus.APPLIED if total_score >= 50: - return "PARTIAL" - return "UNAPPLIED" + return MatchStatus.PARTIAL + return MatchStatus.UNAPPLIED def _format_overlap(tokens: set[str], *, limit: int = 3) -> str: @@ -579,7 +577,7 @@ def calculate_match_score(payload: ScoringInput) -> ScoreBreakdown: type_bonus=0, penalty=0, total=0, - status="UNAPPLIED", + status=MatchStatus.UNAPPLIED, is_opposite_direction=opposite_direction, is_goal_mismatch=True, ) From 18a68de600c9c5fefbe7ff7133975bd8680aadc9 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:04:05 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[refactor/#56]=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=A9=ED=96=A5=20enum=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/domains/commit/services/summarize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/domains/commit/services/summarize.py b/app/domains/commit/services/summarize.py index 5111b4b..9db8a74 100644 --- a/app/domains/commit/services/summarize.py +++ b/app/domains/commit/services/summarize.py @@ -10,6 +10,7 @@ from app.core.chroma import get_commit_collection from app.core.config import settings +from app.core.enums import CommitChangeDirection from app.core.errors import AppServiceError from app.core.gemini import RETRY_STATUS_CODES, generate_content_with_retry from app.domains.commit.schemas import ChangedFile @@ -56,7 +57,7 @@ """.strip() -VALID_DIRECTIONS = {"add", "remove", "modify", "migrate"} +VALID_DIRECTIONS = {direction.value for direction in CommitChangeDirection} PATH_TOKEN_STOPWORDS = { "api", "app", From 167f049028691e4929e4a9394a589ba5f2875fc3 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:04:32 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[refactor/#56]=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=ED=83=80=EC=9E=84=EB=9D=BC=EC=9D=B8=20ste?= =?UTF-8?q?p=20enum=20=EC=B0=B8=EC=A1=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting_analysis/services/embedding.py | 13 ++++++++-- .../meeting_analysis/services/extraction.py | 16 ++++++++++--- tests/test_application_embedding.py | 24 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/domains/meeting_analysis/services/embedding.py b/app/domains/meeting_analysis/services/embedding.py index 84be544..37c9a2e 100644 --- a/app/domains/meeting_analysis/services/embedding.py +++ b/app/domains/meeting_analysis/services/embedding.py @@ -5,6 +5,7 @@ from app.core.chroma import get_application_collection from app.core.config import settings +from app.core.enums import TimelineStep from app.core.errors import AppServiceError from app.domains.meeting_analysis.schemas import ( Application, @@ -16,8 +17,9 @@ _meeting_locks: dict[str, asyncio.Lock] = {} _meeting_locks_guard = asyncio.Lock() TIMELINE_CONTEXT_MAX_LENGTH = 400 -TIMELINE_AGREEMENT_STEP = "적용합의" -TIMELINE_DISCUSSION_STEP = "대안논의" +TIMELINE_AGREEMENT_STEP = TimelineStep.AGREEMENT.value +TIMELINE_DISCUSSION_STEP = TimelineStep.DISCUSSION.value +KNOWN_TIMELINE_STEPS = {step.value for step in TimelineStep} async def _get_meeting_lock(meeting_id: str) -> asyncio.Lock: @@ -81,6 +83,13 @@ def _build_timeline_context(application: Application) -> tuple[str, str]: discussions: list[str] = [] seen: set[str] = set() used_length = 0 + unknown_steps = { + item.step + for item in application.timeline + if item.step not in KNOWN_TIMELINE_STEPS + } + for step in sorted(unknown_steps): + logger.warning("Unknown timeline step skipped: %r", step) for target_step, target_values in ( (TIMELINE_AGREEMENT_STEP, agreements), diff --git a/app/domains/meeting_analysis/services/extraction.py b/app/domains/meeting_analysis/services/extraction.py index 95d1363..d1e979b 100644 --- a/app/domains/meeting_analysis/services/extraction.py +++ b/app/domains/meeting_analysis/services/extraction.py @@ -7,6 +7,7 @@ from google.genai import types from pydantic import BaseModel, Field +from app.core.enums import TimelineStep from app.core.errors import AppServiceError from app.core.gemini import generate_content_with_retry from app.domains.meeting_analysis.schemas import ( @@ -143,7 +144,10 @@ ' "timeline": [', " {", ' "timestamp": "...",', - ' "step": "이슈제기/대안논의/적용합의",', + ( + f' "step": "{TimelineStep.ISSUE.value}/' + f'{TimelineStep.DISCUSSION.value}/{TimelineStep.AGREEMENT.value}",' + ), ' "member_id": 1,', ' "content": "간략 요약 한 문장",', ' "utterance": "실제 발화 원문"', @@ -205,7 +209,10 @@ *NOUN_ENDING_REASON_RULE, "", "[정책 3: 타임라인]", - "1. 이슈제기 -> 대안논의 -> 적용합의 순서를 따른다.", + ( + f"1. {TimelineStep.ISSUE.value} -> {TimelineStep.DISCUSSION.value} " + f"-> {TimelineStep.AGREEMENT.value} 순서를 따른다." + ), "2. 실제 발화 원문과 member_id를 포함한다.", "3. member_id는 STT 데이터의 member_id 값을 사용하고, 없으면 null로 둔다.", "4. content는 간략한 한 문장으로 작성한다.", @@ -220,7 +227,10 @@ ' "timeline": [', " {", ' "timestamp": "...",', - ' "step": "이슈제기/대안논의/적용합의",', + ( + f' "step": "{TimelineStep.ISSUE.value}/' + f'{TimelineStep.DISCUSSION.value}/{TimelineStep.AGREEMENT.value}",' + ), ' "member_id": 1,', ' "content": "간략 요약 한 문장",', ' "utterance": "실제 발화 원문"', diff --git a/tests/test_application_embedding.py b/tests/test_application_embedding.py index c8fb955..352339a 100644 --- a/tests/test_application_embedding.py +++ b/tests/test_application_embedding.py @@ -285,6 +285,30 @@ def test_timeline_context_skips_blank_content(self): assert "결론:" not in docs[0].text assert "논의:" not in docs[0].text + def test_unknown_timeline_step_is_skipped_with_warning(self, caplog): + """정의되지 않은 timeline step은 에러 대신 경고 후 제외한다.""" + application = Application( + application_title="타임라인 step 변형 처리", + application_reasons=["LLM 변형 응답으로 인한 실패를 막는다."], + timeline=[ + ApplicationTimelineItem( + timestamp="00:00:20", + step="적용 합의", + member_id=3, + content="공백이 들어간 step은 제외", + utterance="합의했습니다", + ), + ], + ) + + with caplog.at_level("WARNING"): + docs = build_embedding_documents( + "mtg-unknown-step", _make_result([application]) + ) + + assert "결론:" not in docs[0].text + assert "Unknown timeline step skipped: '적용 합의'" in caplog.text + def _mock_embedding(n: int, dim: int = 768) -> list[list[float]]: return [[0.1] * dim for _ in range(n)] From 96e33cf54e3226756edaac2adf3e840e338acc3e Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Thu, 14 May 2026 21:04:57 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[test/#56]=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?enum=20API=20=EA=B3=84=EC=95=BD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_domain_enums.py | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_domain_enums.py diff --git a/tests/test_domain_enums.py b/tests/test_domain_enums.py new file mode 100644 index 0000000..a293fbf --- /dev/null +++ b/tests/test_domain_enums.py @@ -0,0 +1,76 @@ +"""공통 도메인 enum 계약 테스트.""" + +from app.core.enums import ( + CommitAnalyzeRunPhase, + CommitChangeDirection, + RunStatus, + TranscribeRunPhase, +) +from app.main import app + + +def test_openapi_exposes_run_status_as_named_string_enum(): + """run 상태 enum 값은 API 계약 문자열과 일치한다.""" + schemas = app.openapi()["components"]["schemas"] + + assert schemas["RunStatus"] == { + "type": "string", + "enum": ["queued", "processing", "completed", "failed"], + "title": "RunStatus", + } + assert schemas["TranscribeRunPhase"] == { + "type": "string", + "enum": [ + "queued", + "transcribing", + "transcript_ready", + "summary_ready", + "applications_ready", + "failed", + ], + "title": "TranscribeRunPhase", + } + assert schemas["CommitAnalyzeRunPhase"] == { + "type": "string", + "enum": [ + "queued", + "summarizing", + "summary_ready", + "embedding", + "embedding_ready", + "failed", + ], + "title": "CommitAnalyzeRunPhase", + } + + +def test_enum_values_match_existing_api_contracts(): + """공통 enum 값은 기존 외부 계약 문자열을 유지한다.""" + assert [status.value for status in RunStatus] == [ + "queued", + "processing", + "completed", + "failed", + ] + assert [phase.value for phase in TranscribeRunPhase] == [ + "queued", + "transcribing", + "transcript_ready", + "summary_ready", + "applications_ready", + "failed", + ] + assert [phase.value for phase in CommitAnalyzeRunPhase] == [ + "queued", + "summarizing", + "summary_ready", + "embedding", + "embedding_ready", + "failed", + ] + assert {direction.value for direction in CommitChangeDirection} == { + "add", + "remove", + "modify", + "migrate", + }