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
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""add report generation runs drafts and events tables"""

revision = '5a556853a0e7'
down_revision = '838ba1ab5f45'
branch_labels = None
depends_on = None

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('report_generation_runs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('session_id', sa.UUID(), nullable=False),
sa.Column('patient_ref_id', sa.UUID(), nullable=False),
sa.Column('therapist_id', sa.UUID(), nullable=False),
sa.Column('status', sa.String(length=32), nullable=False),
sa.Column('current_node', sa.String(length=64), nullable=True),
sa.Column('agent_runtime', sa.String(length=64), server_default='agentcore', nullable=False),
sa.Column('graph_version', sa.String(length=64), nullable=False),
sa.Column('context_agent_version', sa.String(length=64), nullable=True),
sa.Column('research_agent_version', sa.String(length=64), nullable=True),
sa.Column('critic_agent_version', sa.String(length=64), nullable=True),
sa.Column('report_agent_version', sa.String(length=64), nullable=True),
sa.Column('qa_agent_version', sa.String(length=64), nullable=True),
sa.Column('model_config_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('retry_budget_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('quality_scores_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('failure_code', sa.String(length=128), nullable=True),
sa.Column('failure_detail', sa.Text(), nullable=True),
sa.Column('trace_id', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['patient_ref_id'], ['patient_refs.id'], name=op.f('fk_report_generation_runs_patient_ref_id_patient_refs'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], name=op.f('fk_report_generation_runs_session_id_sessions'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['therapist_id'], ['users.id'], name=op.f('fk_report_generation_runs_therapist_id_users'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_report_generation_runs'))
)
op.create_table('report_drafts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('generation_run_id', sa.UUID(), nullable=False),
sa.Column('session_id', sa.UUID(), nullable=False),
sa.Column('patient_ref_id', sa.UUID(), nullable=False),
sa.Column('therapist_id', sa.UUID(), nullable=False),
sa.Column('status', sa.String(length=32), server_default='DRAFT', nullable=False),
sa.Column('soap_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('reasoning_summary_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('claims_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('evidence_trace_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('validation_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('quality_scores_json', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['generation_run_id'], ['report_generation_runs.id'], name=op.f('fk_report_drafts_generation_run_id_report_generation_runs'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['patient_ref_id'], ['patient_refs.id'], name=op.f('fk_report_drafts_patient_ref_id_patient_refs'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], name=op.f('fk_report_drafts_session_id_sessions'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['therapist_id'], ['users.id'], name=op.f('fk_report_drafts_therapist_id_users'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_report_drafts'))
)
op.create_table('report_generation_events',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('run_id', sa.UUID(), nullable=False),
sa.Column('node_name', sa.String(length=64), nullable=False),
sa.Column('event_type', sa.String(length=64), nullable=False),
sa.Column('decision', sa.String(length=64), nullable=True),
sa.Column('retry_count', sa.Integer(), nullable=True),
sa.Column('duration_ms', sa.BigInteger(), nullable=True),
sa.Column('token_usage_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('metadata_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['run_id'], ['report_generation_runs.id'], name=op.f('fk_report_generation_events_run_id_report_generation_runs'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_report_generation_events'))
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('report_generation_events')
op.drop_table('report_drafts')
op.drop_table('report_generation_runs')
# ### end Alembic commands ###
20 changes: 20 additions & 0 deletions app/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,23 @@ class ReportPatchStatus(StrEnum):
REJECTED = "REJECTED"


class ReportGenerationRunStatus(StrEnum):
"""report_generation_runs.status. plain VARCHAR 컬럼이라 pg ENUM으로 강제하지
않고 애플리케이션 레벨에서만 검증한다 (5-Agent Graph가 값을 자주 갱신하므로
ENUM 타입 변경 마이그레이션 비용을 피한다)."""
QUEUED = "QUEUED"
RUNNING = "RUNNING"
RESEARCHING = "RESEARCHING"
WRITING = "WRITING"
VALIDATING = "VALIDATING"
HUMAN_REVIEW = "HUMAN_REVIEW"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"


class ReportDraftStatus(StrEnum):
"""report_drafts.status. Agent는 DRAFT만 쓸 수 있고, 최종 확정은
치료사가 기존 reports 워크플로에서 수행한다."""
DRAFT = "DRAFT"


101 changes: 101 additions & 0 deletions app/models/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,104 @@ class PatientMetricTrend(Base):
metric: Mapped[str] = mapped_column(String(100), nullable=False)
trend_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)


class ReportGenerationRun(Base):
"""Strands Graph 기반 5-Agent 리포트 생성 1회 실행 기록.

status는 pg ENUM이 아니라 plain VARCHAR다 — Graph 실행 중 자주 갱신되므로
ENUM 타입 변경 마이그레이션 비용을 피한다. 값은 ReportGenerationRunStatus로
애플리케이션 레벨에서 검증한다.
"""

__tablename__ = "report_generation_runs"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="RESTRICT"), nullable=False
)
patient_ref_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("patient_refs.id", ondelete="RESTRICT"), nullable=False
)
therapist_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
)

status: Mapped[str] = mapped_column(String(32), nullable=False)
current_node: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_runtime: Mapped[str] = mapped_column(String(64), nullable=False, default="agentcore", server_default="agentcore")
graph_version: Mapped[str] = mapped_column(String(64), nullable=False)

context_agent_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
research_agent_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
critic_agent_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
report_agent_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
qa_agent_version: Mapped[str | None] = mapped_column(String(64), nullable=True)

model_config_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
retry_budget_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
quality_scores_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
failure_code: Mapped[str | None] = mapped_column(String(128), nullable=True)
failure_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
trace_id: Mapped[str | None] = mapped_column(String(255), nullable=True)

created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)


class ReportDraft(Base):
"""5-Agent Graph가 생성한 SOAP 리포트 초안.

QA/deterministic validation을 모두 통과한 결과만 status='DRAFT'로 저장된다.
Agent는 이 테이블을 최종 확정(FINALIZED)하지 않는다 — 치료사가 기존
reports 워크플로에서 검토 후 확정한다. report_draft_revisions와는 무관하다
(그쪽은 이미 확정된 reports 행의 수정 이력).
"""

__tablename__ = "report_drafts"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
generation_run_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("report_generation_runs.id", ondelete="RESTRICT"), nullable=False
)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="RESTRICT"), nullable=False
)
patient_ref_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("patient_refs.id", ondelete="RESTRICT"), nullable=False
)
therapist_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
)

status: Mapped[str] = mapped_column(String(32), nullable=False, default="DRAFT", server_default="DRAFT")
soap_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
reasoning_summary_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
claims_json: Mapped[list] = mapped_column(JSONB, nullable=False)
evidence_trace_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
validation_json: Mapped[dict] = mapped_column(JSONB, nullable=False)
quality_scores_json: Mapped[dict] = mapped_column(JSONB, nullable=False)

created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)


class ReportGenerationEvent(Base):
"""report_generation_runs의 노드별 진행 이벤트. 민감 원문 없이 실행 상태만 기록한다."""

__tablename__ = "report_generation_events"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
run_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("report_generation_runs.id", ondelete="CASCADE"), nullable=False
)
node_name: Mapped[str] = mapped_column(String(64), nullable=False)
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
decision: Mapped[str | None] = mapped_column(String(64), nullable=True)
retry_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
duration_ms: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
token_usage_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
metadata_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)

created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
6 changes: 6 additions & 0 deletions app/models/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
PatientRef,
RefreshToken,
Report,
ReportDraft,
ReportGenerationEvent,
ReportGenerationRun,
ReportSegment,
Session,
SoapCaseIndex,
Expand All @@ -31,6 +34,9 @@
"PatientRef",
"RefreshToken",
"Report",
"ReportDraft",
"ReportGenerationEvent",
"ReportGenerationRun",
"ReportSegment",
"Session",
"SoapCaseIndex",
Expand Down
Loading