Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions app/core/enums.py
Original file line number Diff line number Diff line change
@@ -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"
20 changes: 4 additions & 16 deletions app/domains/commit/schemas.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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" 고정으로 시작)'
)
Expand All @@ -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. "
Expand Down
33 changes: 16 additions & 17 deletions app/domains/commit/services/analyze_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -27,7 +26,7 @@
@dataclass
class _CommitAnalyzeRunRecord:
run_id: str
status: CommitAnalyzeRunStatusValue
status: RunStatus
phase: CommitAnalyzeRunPhase
request: CommitAnalyzeRequest
submitted_at: datetime
Expand Down Expand Up @@ -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:
Expand All @@ -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,
)
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion app/domains/commit/services/summarize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,7 +57,7 @@
""".strip()


VALID_DIRECTIONS = {"add", "remove", "modify", "migrate"}
VALID_DIRECTIONS = {direction.value for direction in CommitChangeDirection}
PATH_TOKEN_STOPWORDS = {
"api",
"app",
Expand Down
13 changes: 11 additions & 2 deletions app/domains/meeting_analysis/services/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand Down
16 changes: 13 additions & 3 deletions app/domains/meeting_analysis/services/extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -143,7 +144,10 @@
' "timeline": [',
" {",
' "timestamp": "...",',
' "step": "이슈제기/대안논의/적용합의",',
(
f' "step": "{TimelineStep.ISSUE.value}/'
f'{TimelineStep.DISCUSSION.value}/{TimelineStep.AGREEMENT.value}",'
),
' "member_id": 1,',
' "content": "간략 요약 한 문장",',
' "utterance": "실제 발화 원문"',
Expand Down Expand Up @@ -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는 간략한 한 문장으로 작성한다.",
Expand All @@ -220,7 +227,10 @@
' "timeline": [',
" {",
' "timestamp": "...",',
' "step": "이슈제기/대안논의/적용합의",',
(
f' "step": "{TimelineStep.ISSUE.value}/'
f'{TimelineStep.DISCUSSION.value}/{TimelineStep.AGREEMENT.value}",'
),
' "member_id": 1,',
' "content": "간략 요약 한 문장",',
' "utterance": "실제 발화 원문"',
Expand Down
Loading
Loading