48f1027f08
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>
139 lines
5.0 KiB
Python
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,
|
|
}
|