feat: Phase 2 구현 — AI 에이전트 4인방, EVMS, Vision AI, Geofence
AI 에이전트 (Layer 2): - GONGSA: 공사 담당 (공정 브리핑, 공기 지연 감지, 날씨 연동 작업 조정) - PUMJIL: 품질 담당 (시공 전 체크리스트, Vision 보조 판독, 시험 기한 추적) - ANJEON: 안전 담당 (위험 공정 경보, TBM 생성, 중대재해처벌법 Q&A) - GUMU: 공무 담당 (인허가 능동 추적, 기성청구 제안, 보고서 초안) - 에이전트 라우터 (키워드 기반 자동 분배), 아침 브리핑 엔드포인트 EVMS 기본: - PV·EV·AC·SPI·CPI 산출 (WBS/Task 기반) - EAC·ETC 예측, 스냅샷 이력 저장 Vision AI: - Level 1: 현장 사진 분류 (Claude Vision), 작업일보 자동 첨부 - Level 2: 안전장비(안전모/조끼) 착용 감지 Geofence 위험구역: - 구역 CRUD (굴착면, 크레인 반경, 밀폐공간 등) - 진입 이벤트 웹훅 (익명 — 개인 이동 경로 비수집) 인허가 자동도출: - 공종 입력 → AI가 필요 인허가 목록 자동 도출 + 체크리스트 생성 DB 마이그레이션 (002): - agent_conversations, agent_messages, evms_snapshots, geofence_zones Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ from .weather import WeatherData, WeatherAlert
|
||||
from .permit import PermitItem
|
||||
from .rag import RagSource, RagChunk
|
||||
from .settings import ClientProfile, AlertRule, WorkTypeLibrary
|
||||
from .agent import AgentConversation, AgentMessage, GeofenceZone, AgentType
|
||||
from .evms import EVMSSnapshot
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -22,4 +24,6 @@ __all__ = [
|
||||
"PermitItem",
|
||||
"RagSource", "RagChunk",
|
||||
"ClientProfile", "AlertRule", "WorkTypeLibrary",
|
||||
"AgentConversation", "AgentMessage", "GeofenceZone", "AgentType",
|
||||
"EVMSSnapshot",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import uuid
|
||||
import enum
|
||||
from sqlalchemy import String, Text, ForeignKey, Enum as SAEnum, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
|
||||
|
||||
class AgentType(str, enum.Enum):
|
||||
GONGSA = "gongsa" # 공사 담당
|
||||
PUMJIL = "pumjil" # 품질 담당
|
||||
ANJEON = "anjeon" # 안전 담당
|
||||
GUMU = "gumu" # 공무 담당
|
||||
|
||||
|
||||
class ConversationStatus(str, enum.Enum):
|
||||
ACTIVE = "active"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class AgentConversation(Base, UUIDMixin, TimestampMixin):
|
||||
"""에이전트와의 대화 세션"""
|
||||
__tablename__ = "agent_conversations"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
agent_type: Mapped[AgentType] = mapped_column(SAEnum(AgentType, name="agent_type"), nullable=False)
|
||||
title: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
status: Mapped[ConversationStatus] = mapped_column(
|
||||
SAEnum(ConversationStatus, name="conversation_status"),
|
||||
default=ConversationStatus.ACTIVE,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project")
|
||||
user: Mapped["User"] = relationship("User")
|
||||
messages: Mapped[list["AgentMessage"]] = relationship(
|
||||
"AgentMessage", back_populates="conversation", cascade="all, delete-orphan",
|
||||
order_by="AgentMessage.created_at",
|
||||
)
|
||||
|
||||
|
||||
class AgentMessage(Base, UUIDMixin, TimestampMixin):
|
||||
"""에이전트 대화 메시지"""
|
||||
__tablename__ = "agent_messages"
|
||||
|
||||
conversation_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("agent_conversations.id"), nullable=False
|
||||
)
|
||||
role: Mapped[str] = mapped_column(String(20), nullable=False) # user | assistant | system
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True) # action proposals, references, etc.
|
||||
is_proactive: Mapped[bool] = mapped_column(Boolean, default=False) # 에이전트가 먼저 보낸 메시지
|
||||
|
||||
# relationships
|
||||
conversation: Mapped["AgentConversation"] = relationship("AgentConversation", back_populates="messages")
|
||||
|
||||
|
||||
class GeofenceZone(Base, UUIDMixin, TimestampMixin):
|
||||
"""익명 위험구역 Geofence"""
|
||||
__tablename__ = "geofence_zones"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False) # "3공구 굴착면", "크레인 반경"
|
||||
zone_type: Mapped[str] = mapped_column(String(50), nullable=False) # excavation, crane, confined_space
|
||||
coordinates: Mapped[list] = mapped_column(JSONB, nullable=False) # [[lat,lng], ...]
|
||||
radius_m: Mapped[float | None] = mapped_column(nullable=True) # 원형 구역용 반경(m)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project")
|
||||
@@ -0,0 +1,49 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from sqlalchemy import Date, Float, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
|
||||
|
||||
class EVMSSnapshot(Base, UUIDMixin, TimestampMixin):
|
||||
"""
|
||||
EVMS 공정 성과 스냅샷 (기준일 기준 PV/EV/AC 저장)
|
||||
|
||||
PV (Planned Value) = 계획 공정률 기준 예산 투입액
|
||||
EV (Earned Value) = 실제 완료 공정률 기준 예산 투입액
|
||||
AC (Actual Cost) = 실제 투입 비용
|
||||
SPI = EV / PV (1 이상: 공정 앞서감, 미만: 지연)
|
||||
CPI = EV / AC (1 이상: 비용 효율적, 미만: 비용 초과)
|
||||
"""
|
||||
__tablename__ = "evms_snapshots"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
snapshot_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
||||
|
||||
# 예산 (원)
|
||||
total_budget: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
# 공정률 (%)
|
||||
planned_progress: Mapped[float | None] = mapped_column(Float, nullable=True) # 계획 공정률
|
||||
actual_progress: Mapped[float | None] = mapped_column(Float, nullable=True) # 실제 공정률
|
||||
|
||||
# EVM 핵심 지표 (원)
|
||||
pv: Mapped[float | None] = mapped_column(Float, nullable=True) # Planned Value
|
||||
ev: Mapped[float | None] = mapped_column(Float, nullable=True) # Earned Value
|
||||
ac: Mapped[float | None] = mapped_column(Float, nullable=True) # Actual Cost
|
||||
|
||||
# 파생 지수
|
||||
spi: Mapped[float | None] = mapped_column(Float, nullable=True) # Schedule Performance Index
|
||||
cpi: Mapped[float | None] = mapped_column(Float, nullable=True) # Cost Performance Index
|
||||
|
||||
# 예측
|
||||
eac: Mapped[float | None] = mapped_column(Float, nullable=True) # Estimate at Completion
|
||||
etc: Mapped[float | None] = mapped_column(Float, nullable=True) # Estimate to Complete
|
||||
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
detail_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True) # WBS별 세부 내역
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project")
|
||||
Reference in New Issue
Block a user