Files
sinmb79 48f1027f08 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>
2026-03-24 21:49:44 +09:00

139 lines
5.0 KiB
Python

"""
에이전트 베이스 클래스
각 에이전트는 독립된 페르소나 + Claude 인스턴스로 구동됩니다.
- 엔진 DB를 공유하여 프로젝트 데이터 접근
- 에이전트는 제안하고, 사람이 결정합니다
- 모든 대화는 DB에 기록됩니다
"""
from abc import ABC, abstractmethod
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.ai_engine import complete
class BaseAgent(ABC):
"""모든 에이전트의 공통 베이스"""
agent_type: str
name_ko: str
@property
@abstractmethod
def system_prompt(self) -> str:
"""에이전트 고유 시스템 프롬프트"""
...
async def chat(
self,
messages: list[dict],
context: dict | None = None,
temperature: float = 0.5,
) -> str:
"""
대화 수행
messages: [{"role": "user"|"assistant", "content": "..."}]
context: 프로젝트/날씨/태스크 등 컨텍스트 (시스템 프롬프트에 주입)
"""
system = self.system_prompt
if context:
system += "\n\n## 현재 컨텍스트\n" + self._format_context(context)
return await complete(
messages=messages,
system=system,
temperature=temperature,
max_tokens=2048,
)
def _format_context(self, context: dict) -> str:
lines = []
if context.get("project_name"):
lines.append(f"- 프로젝트: {context['project_name']}")
if context.get("today"):
lines.append(f"- 오늘 날짜: {context['today']}")
if context.get("weather"):
lines.append(f"- 오늘 날씨: {context['weather']}")
if context.get("active_tasks"):
lines.append(f"- 진행 중 공종: {', '.join(context['active_tasks'])}")
if context.get("pending_inspections"):
lines.append(f"- 미완료 검측: {context['pending_inspections']}")
if context.get("overdue_tests"):
lines.append(f"- 기한 초과 품질시험: {context['overdue_tests']}")
if context.get("overdue_permits"):
lines.append(f"- 지연 인허가: {context['overdue_permits']}")
if context.get("schedule_delay_days"):
lines.append(f"- 공정 지연: {context['schedule_delay_days']}")
return "\n".join(lines)
async def build_context(self, db: AsyncSession, project_id: str) -> dict:
"""프로젝트 컨텍스트를 DB에서 조회하여 반환"""
from datetime import date
from sqlalchemy import select, func
from app.models.project import Project
from app.models.task import Task
from app.models.weather import WeatherData
from app.models.inspection import InspectionRequest, InspectionStatus
from app.models.permit import PermitItem
import uuid
pid = uuid.UUID(str(project_id))
# 프로젝트 정보
proj_r = await db.execute(select(Project).where(Project.id == pid))
project = proj_r.scalar_one_or_none()
if not project:
return {}
# 오늘 날씨
today = date.today()
weather_r = await db.execute(
select(WeatherData).where(WeatherData.project_id == pid, WeatherData.forecast_date == today)
)
weather = weather_r.scalar_one_or_none()
weather_summary = None
if weather:
parts = []
if weather.sky_condition:
parts.append(weather.sky_condition)
if weather.temperature_max is not None:
parts.append(f"최고 {weather.temperature_max:.0f}°C")
if weather.precipitation_mm and weather.precipitation_mm > 0:
parts.append(f"강수 {weather.precipitation_mm:.1f}mm")
weather_summary = " / ".join(parts) if parts else None
# 진행 중 태스크
tasks_r = await db.execute(
select(Task).where(Task.project_id == pid, Task.status.in_(["in_progress", "not_started"]))
)
tasks = tasks_r.scalars().all()
active_tasks = list({t.name for t in tasks if t.status == "in_progress"})[:5]
# 미완료 검측
insp_r = await db.execute(
select(func.count()).where(
InspectionRequest.project_id == pid,
InspectionRequest.status == InspectionStatus.DRAFT,
)
)
pending_inspections = insp_r.scalar() or 0
# 지연 인허가
permit_r = await db.execute(
select(func.count()).where(
PermitItem.project_id == pid,
PermitItem.status.in_(["pending", "in_progress"]),
PermitItem.due_date < today,
)
)
overdue_permits = permit_r.scalar() or 0
return {
"project_name": project.name,
"today": str(today),
"weather": weather_summary,
"active_tasks": active_tasks,
"pending_inspections": pending_inspections,
"overdue_permits": overdue_permits,
}