Files
conai/backend/app/services/kakao_service.py
sinmb79 2a4950d8a0 feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼

Backend (FastAPI):
- SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings)
- API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings)
- Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API
- Alembic 마이그레이션 (pgvector 포함)
- pytest 테스트 (CPM, 날씨 경보)

Frontend (Next.js 15):
- 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정)
- TanStack Query + Zustand + Tailwind CSS

인프라:
- Docker Compose (PostgreSQL pgvector + backend + frontend)
- 한국어 README 및 설치 가이드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:06:36 +09:00

124 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Kakao Chatbot Skill API service.
Parses incoming messages and routes to appropriate handlers.
"""
import re
from datetime import date
# Kakao Skill response builders
def simple_text(text: str) -> dict:
return {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": text}}]
}
}
def basic_card(title: str, description: str, buttons: list[dict] | None = None) -> dict:
card = {"title": title, "description": description}
if buttons:
card["buttons"] = buttons
return {
"version": "2.0",
"template": {
"outputs": [{"basicCard": card}]
}
}
def list_card(header_title: str, items: list[dict], buttons: list[dict] | None = None) -> dict:
card = {
"header": {"title": header_title},
"items": items,
}
if buttons:
card["buttons"] = buttons
return {
"version": "2.0",
"template": {
"outputs": [{"listCard": card}]
}
}
# Message routing
class KakaoIntent:
DAILY_REPORT = "daily_report"
RAG_QUESTION = "rag_question"
WEATHER = "weather"
HELP = "help"
UNKNOWN = "unknown"
def detect_intent(utterance: str) -> str:
"""Detect user intent from utterance."""
u = utterance.strip()
# Daily report keywords
if any(k in u for k in ["일보", "작업일보", "오늘 공사", "금일 공사"]):
return KakaoIntent.DAILY_REPORT
# RAG / question keywords
if any(k in u for k in ["질문", "법규", "시방서", "기준", "KCS", "법령", "산안법", "중대재해", "?", ""]):
return KakaoIntent.RAG_QUESTION
# Weather keywords
if any(k in u for k in ["날씨", "기상", "", "", "바람"]):
return KakaoIntent.WEATHER
# Help
if any(k in u for k in ["도움말", "메뉴", "help", "사용법"]):
return KakaoIntent.HELP
return KakaoIntent.UNKNOWN
def parse_daily_report_input(utterance: str) -> dict:
"""
Parse daily report input from free-form Kakao message.
Example: "오늘 일보: 콘크리트 5명, 철근 3명, 관로매설 오후 완료"
"""
workers = {}
work_items = []
issues = None
# Extract worker counts: "직종 N명" patterns
worker_pattern = re.findall(r'([가-힣a-zA-Z]+)\s+(\d+)명', utterance)
for role, count in worker_pattern:
if role not in ["", "합계"]:
workers[role] = int(count)
# Extract work items after "일보:" or newlines
lines = utterance.replace("일보:", "").replace("작업일보:", "").split("\n")
for line in lines:
line = line.strip().lstrip("-").strip()
if line and len(line) > 2 and not re.search(r'\d+명', line):
work_items.append(line)
# Check for issues
if "특이" in utterance or "문제" in utterance or "이슈" in utterance:
issue_match = re.search(r'(특이|문제|이슈)[사항:\s]*(.+?)(?:\n|$)', utterance)
if issue_match:
issues = issue_match.group(2).strip()
return {
"workers_count": workers,
"work_items": work_items if work_items else ["기타 작업"],
"issues": issues,
"report_date": str(date.today()),
}
def make_help_response() -> dict:
return list_card(
header_title="CONAI 현장 도우미",
items=[
{"title": "작업일보 작성", "description": "일보: 작업내용 입력"},
{"title": "법규 질문", "description": "질문: 궁금한 내용 입력"},
{"title": "날씨 확인", "description": "날씨 입력"},
],
buttons=[{"action": "message", "label": "일보 작성", "messageText": "일보:"}],
)