- intent_service.py: PERSONA_FULL/PERSONA_LOCAL 상수 정의 + GET /persona 엔드포인트 추가 - 기존 ID_SYSTEM_PROMPT (반말) 제거, PERSONA_LOCAL (존댓말)로 교체 - [자아], [기능 범위] 섹션 추가로 Opus 거짓 응답 방지 - n8n: Call Qwen/Haiku/Opus 3개 노드 → /persona 엔드포인트 런타임 fetch로 전환 - 각 노드에 httpGet 함수 + fallback 프롬프트 추가 - Haiku의 [기능 범위] 별도 추가 코드 제거 (PERSONA_FULL에 이미 포함) - docker-compose.yml: INTENT_SERVICE_URL 환경변수 추가 - manage_services.sh: intent-service를 SERVICES 배열에 추가 - init/migrate-v8.sql: DB ai_configs/prompts 표시용 동기화 - init/init.sql: 시드 데이터 동기화 + "표시용" 주석 추가 - docs/architecture.md: 페르소나 섹션 업데이트 (단일 소스 명시) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
443 lines
20 KiB
Markdown
443 lines
20 KiB
Markdown
# Architecture
|
|
|
|
## 전체 아키텍처
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ Synology Chat │ 사용자 인터페이스
|
|
│ (NAS 192.168.1.227)│
|
|
└─────────┬───────────┘
|
|
│ Outgoing Webhook
|
|
▼
|
|
┌────────────────────────────────────────────────────────────┐
|
|
│ bot-n8n (맥미니 Docker :5678) — 51노드 파이프라인 │
|
|
│ │
|
|
│ ⓪ 토큰 검증 + Rate Limit (username별 10초/5건) │
|
|
│ │
|
|
│ ① 규칙 기반 프리필터 │
|
|
│ └─ 인사/감사 정규식 매칭 → 하드코딩 local 응답 │
|
|
│ │
|
|
│ ② 명령어 체크 (/설정, /모델, /성격, /문서등록, /보고서) │
|
|
│ └─ 권한 체크 (ADMIN_USERNAMES allowlist) │
|
|
│ │
|
|
│ ③ GPU Qwen 9B 분류 v3 (10초 타임아웃) │
|
|
│ → {intent, response_tier, needs_rag, rag_target, ...} │
|
|
│ └─ 실패 시 fallback → api_light │
|
|
│ │
|
|
│ ③-1 Route by Intent │
|
|
│ ├─ log_event → Qwen 추출 → bge-m3 → tk_company 저장 │
|
|
│ ├─ report → 현장 리포트 → field_reports DB 저장 │
|
|
│ ├─ calendar → CalDAV Bridge → Synology Calendar │
|
|
│ ├─ reminder → calendar로 통합 처리 │
|
|
│ ├─ mail → 메일 요약 조회 (mail_logs) │
|
|
│ ├─ note → KB Writer → 문서 저장 │
|
|
│ └─ fallback → 일반 대화 (RAG + 3단계 라우팅) │
|
|
│ │
|
|
│ ④ [needs_rag=true] 멀티-컬렉션 RAG 검색 │
|
|
│ documents + tk_company + chat_memory │
|
|
│ → bge-m3 임베딩 → Qdrant 검색 → reranker → top-3 │
|
|
│ │
|
|
│ ⑤ response_tier 3단계 분기 │
|
|
│ ├─ local → Qwen 9B 직접 답변 │
|
|
│ ├─ api_light → Claude Haiku │
|
|
│ └─ api_heavy → 예산 체크 → Claude Opus (or 다운그레이드) │
|
|
│ │
|
|
│ ⑥ 응답 전송 + chat_logs + api_usage_monthly │
|
|
│ ⑦ [비동기] Qwen 메모리 판단 → 가치 있으면 벡터화 + KB 저장 │
|
|
│ └─ classification_logs 기록 │
|
|
└──┬──────────┬───────────┬───────────┬──────────────────────┘
|
|
│ │ │ │
|
|
▼ ▼ ▼ ▼
|
|
┌──────┐ ┌────────┐ ┌─────────┐ ┌──────────────┐
|
|
│bot- │ │Qdrant │ │Ollama │ │Ollama (GPU) │
|
|
│postgres│ │:6333 │ │:11434 │ │192.168.1.186 │
|
|
│:15478│ │3컬렉션 │ │bge-m3 │ │:11434 │
|
|
│12테이블│ │documents│ │reranker│ │qwen3.5:9b │
|
|
│ │ │tk_company││비전모델 │ │(분류+응답) │
|
|
│ │ │chat_memory│ │ │ │
|
|
└──────┘ └────────┘ └─────────┘ └──────────────┘
|
|
|
|
┌────────────────────────────────────────────────────────────┐
|
|
│ 별도 워크플로우 │
|
|
│ Mail Processing Pipeline (7노드) │
|
|
│ └─ MailPlus IMAP 폴링 → Qwen 분류 → mail_logs 저장 │
|
|
└────────────────────────────────────────────────────────────┘
|
|
|
|
┌────────────────────────────────────────────────────────────┐
|
|
│ 네이티브 서비스 (맥미니) │
|
|
│ │
|
|
│ heic_converter.py (:8090) │
|
|
│ └─ HEIC→JPEG 변환 (macOS sips) │
|
|
│ │
|
|
│ chat_bridge.py (:8091) │
|
|
│ ├─ DSM Chat API 폴링 (5초) → 사진 감지 + ack │
|
|
│ ├─ POST /chat/recent-photo → 사진 다운+변환 │
|
|
│ └─ HEIC 자동 변환 (→ heic_converter :8090) │
|
|
│ │
|
|
│ caldav_bridge.py (:8092) │
|
|
│ └─ CalDAV REST 래퍼 (Synology Calendar CRUD) │
|
|
│ │
|
|
│ inbox_processor.py (LaunchAgent, 5분) │
|
|
│ └─ OmniFocus Inbox 폴링 → Qwen 분류 → 자동 정리 │
|
|
│ │
|
|
│ news_digest.py (LaunchAgent, 매일 07:00) │
|
|
│ └─ RSS 뉴스 수집 → Qwen 번역·요약 → Qdrant + Synology Chat│
|
|
│ │
|
|
│ morning_briefing.py (LaunchAgent, 매일 07:30) │
|
|
│ └─ 일정·메일·보고·뉴스 → 요약 → Synology Chat 전송 │
|
|
└────────────────────────────────────────────────────────────┘
|
|
|
|
┌────────────────────────────────────────────────┐
|
|
│ NAS 서비스 (192.168.1.227) │
|
|
│ Synology Chat / Calendar (CalDAV) / MailPlus │
|
|
└────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## 3단계 라우팅 상세
|
|
|
|
### response_tier 판단 (Qwen 9B 분류기 v2)
|
|
|
|
| tier | 모델 | 비용 | 대상 |
|
|
|------|------|------|------|
|
|
| **local** | Qwen 9B (GPU) | 무료 | 인사, 잡담, 단순 확인, 감사, 짧은 반응 |
|
|
| **api_light** | Claude Haiku | ~$0.8/1M in, $4/1M out | 요약, 번역, RAG 정리, 비교 분석 |
|
|
| **api_heavy** | Claude Opus | ~$15/1M in, $75/1M out | 법률 해석, 다중 문서 분석, 보고서 작성 |
|
|
|
|
**비용 최적화 목표:**
|
|
- ~40% → local (무료, 프리필터+Qwen 직접 답변)
|
|
- ~50% → Haiku (저비용)
|
|
- ~10% → Opus (복잡한 질문만)
|
|
|
|
### 분류기 v3 출력 스키마
|
|
|
|
```json
|
|
{
|
|
"intent": "greeting|question|log_event|calendar|reminder|mail|note|photo|command|report|other",
|
|
"response_tier": "local|api_light|api_heavy",
|
|
"needs_rag": true,
|
|
"rag_target": ["documents", "tk_company", "chat_memory"],
|
|
"department_hint": "안전|생산|구매|품질|null",
|
|
"report_domain": "안전|시설설비|품질|null",
|
|
"query": "검색용 쿼리"
|
|
}
|
|
```
|
|
|
|
**Intent별 처리:**
|
|
- `calendar` — CalDAV Bridge로 일정 생성/조회 (Synology Calendar)
|
|
- `reminder` — calendar로 통합 (알림 시간 포함 일정 생성)
|
|
- `mail` — mail_logs에서 최근 메일 요약 조회
|
|
- `note` — KB Writer로 문서 저장
|
|
```
|
|
|
|
### 프리필터 → 분류기 → 모델 라우팅 흐름
|
|
|
|
```
|
|
메시지 수신
|
|
│
|
|
├─ 프리필터 매칭 (인사/감사 정규식)
|
|
│ └─ 매칭 → 하드코딩 local 응답 (GPU 서버 미호출)
|
|
│
|
|
└─ 미매칭 → Qwen 9B 분류기
|
|
├─ response_tier=local → Qwen 9B 직접 답변
|
|
├─ response_tier=api_light → Claude Haiku
|
|
└─ response_tier=api_heavy → 예산 체크
|
|
├─ 예산 내 → Claude Opus
|
|
└─ 초과 → Claude Haiku (다운그레이드)
|
|
```
|
|
|
|
## 3-컬렉션 RAG 상세
|
|
|
|
### 컬렉션 구조
|
|
|
|
| 컬렉션 | 용도 | 벡터 차원 | 필터 |
|
|
|--------|------|----------|------|
|
|
| `documents` | 개인/일반 문서, 메일 요약 | 1024 (bge-m3) | 없음 |
|
|
| `tk_company` | TK 회사 문서, 현장 리포트 | 1024 (bge-m3) | department, year, doc_type |
|
|
| `chat_memory` | 가치 있는 대화 기억 | 1024 (bge-m3) | username, topic, intent |
|
|
|
|
### 멀티-컬렉션 검색 흐름
|
|
|
|
```
|
|
rag_target에 따라 동적 검색:
|
|
- 단일 컬렉션 → top-10
|
|
- 2개 → 각 top-7
|
|
- 3개 → 각 top-5
|
|
|
|
각 컬렉션별 필터:
|
|
- documents: 필터 없음
|
|
- tk_company: department + year 필터
|
|
- chat_memory: username 필터
|
|
|
|
합산 → bge-reranker 리랭킹 (실패 시 Qdrant score 정렬) → top-3
|
|
|
|
출처 표시:
|
|
[회사/안전/절차서] 고소작업 안전절차 - "작업 전 반드시..."
|
|
[개인문서] Safety Engineering - "Workers at height..."
|
|
[이전대화/2026-03-10] "고소작업은 2m 이상..."
|
|
```
|
|
|
|
### tk_company payload
|
|
|
|
```
|
|
text, year(인덱스), department(인덱스), doc_type(인덱스),
|
|
title, source_file, file_hash, chunk_index, total_chunks,
|
|
uploaded_by, created_at(인덱스)
|
|
```
|
|
|
|
### chat_memory payload
|
|
|
|
```
|
|
text, feature, intent, username(인덱스), topic(인덱스), timestamp
|
|
```
|
|
|
|
## 선택적 대화 메모리
|
|
|
|
```
|
|
응답 전송 (즉시)
|
|
↓ [비동기]
|
|
Qwen 9B Memorization Check:
|
|
"저장: 사실 정보, 결정사항, 선호, 지시, 기술 정보"
|
|
"무시: 인사, 잡담, 날씨, 봇이 모른다고 답한 것"
|
|
출력: {"save": true/false, "topic": "general|company|technical|personal"}
|
|
↓
|
|
Should Memorize?
|
|
├─ true → bge-m3 Embed → chat_memory 저장
|
|
└─ false → 스킵 (chat_logs에는 기록됨)
|
|
```
|
|
|
|
- local tier (인사 등)는 메모리 체크 자체를 스킵 (GPU 절약)
|
|
|
|
## DB 스키마 상세
|
|
|
|
### 기존 테이블 (v1)
|
|
|
|
```sql
|
|
-- ai_configs: feature별 모델/프롬프트 독립 관리
|
|
-- feature: 'classifier', 'chat', 'chat_local', 'calendar', 'mail_summary'
|
|
|
|
-- routing_rules: complexity 기반 라우팅 (레거시 호환)
|
|
|
|
-- prompts: 프롬프트 버전 관리
|
|
-- feature: 'classifier' v2 (활성), 'chat_local', 'memorize_check'
|
|
|
|
-- chat_logs: 대화 기록 (v2: +username, +response_tier)
|
|
|
|
-- mail_accounts: 메일 소스 관리 (Phase 6)
|
|
```
|
|
|
|
### 신규 테이블 (v2)
|
|
|
|
```sql
|
|
-- document_ingestion_log: 문서 등록 이력 + 버전 관리
|
|
-- file_hash 중복 체크, doc_group_key로 버전 연결
|
|
-- status: processing/completed/failed/deprecated
|
|
|
|
-- field_reports: 현장 리포트 (안전/시설설비/품질 통합)
|
|
-- domain, category, severity → SLA 자동 계산
|
|
-- due_at 기반 미처리 조회
|
|
|
|
-- classification_logs: 분류기 성능 모니터링
|
|
-- input_text (200자 제한), output_json (JSONB)
|
|
-- fallback_used, latency_ms
|
|
|
|
-- mail_logs: 메일 수신 로그 + 분류
|
|
|
|
-- calendar_events: 캘린더 이벤트
|
|
|
|
-- report_cache: 보고서 캐시 (domain + year_month UNIQUE)
|
|
-- 동일 파라미터 재요청 → 캐시 반환, --force로 재생성
|
|
|
|
-- api_usage_monthly: API 사용량 + 예산 상한
|
|
-- year + month + tier UNIQUE
|
|
-- estimated_cost vs budget_limit 비교 → 다운그레이드
|
|
|
|
-- news_digest_log: 뉴스 요약 이력
|
|
-- source_url UNIQUE, translated_title, summary
|
|
-- published_at, processed_at, qdrant_point_id
|
|
```
|
|
|
|
### calendar_events 확장 (v3)
|
|
|
|
```sql
|
|
-- v3에서 추가된 컬럼:
|
|
-- caldav_uid: CalDAV 이벤트 UID (Synology Calendar 연동)
|
|
-- description: 이벤트 상세 설명
|
|
-- created_by: 생성 출처 (chat, caldav_sync, manual)
|
|
```
|
|
|
|
### SLA 기준표
|
|
|
|
| domain | severity | 처리 기한 |
|
|
|--------|----------|----------|
|
|
| 안전 | 상 | 24시간 |
|
|
| 안전 | 중 | 72시간 |
|
|
| 안전 | 하 | 7일 |
|
|
| 시설설비 | 상 | 48시간 |
|
|
| 시설설비 | 중 | 5일 |
|
|
| 시설설비 | 하 | 14일 |
|
|
| 품질 | 상 | 48시간 |
|
|
| 품질 | 중 | 5일 |
|
|
| 품질 | 하 | 14일 |
|
|
|
|
## 안전장치
|
|
|
|
### 보안
|
|
- **웹훅 토큰 검증**: SYNOLOGY_CHAT_TOKEN 비교, 불일치 → reject
|
|
- **명령어 권한 체크**: ADMIN_USERNAMES allowlist
|
|
- **서비스 포트 격리**: DB/Qdrant는 127.0.0.1 바인딩
|
|
- **classification_logs**: input 200자 제한
|
|
|
|
### 비용 폭주 방지
|
|
- **규칙 기반 프리필터**: GPU 서버 다운 시에도 잡담은 API 미호출
|
|
- **Rate Limit**: username별 10초/5건 (in-memory, workflow static data)
|
|
- **예산 상한**: api_usage_monthly → 초과 시 api_heavy→api_light 다운그레이드
|
|
|
|
### 파이프라인 복원력
|
|
- **분류기 fallback**: Qwen 10초 타임아웃 → {response_tier: "api_light"}
|
|
- **리랭커 fallback**: bge-reranker 실패 → Qdrant score 정렬
|
|
- **비전 모델 fallback**: 사진 분석 실패 → 사용자 설명만으로 구조화
|
|
- **HEIC 자동 변환**: macOS sips 기반 HEIC→JPEG 변환 (heic_converter.py, port 8090)
|
|
- **사진 브릿지**: chat_bridge.py가 DSM Chat API 폴링 → 사진 감지 → ack → n8n 요청 시 다운로드+변환+base64 반환
|
|
- **이미지 base64 변환**: Ollama API는 URL 미지원, chat_bridge가 자동 다운로드 후 base64 전달
|
|
|
|
## 메인 채팅 파이프라인 v3 (51노드)
|
|
|
|
```
|
|
Webhook POST /chat
|
|
│
|
|
▼
|
|
[Parse Input] — 토큰 검증 + Rate Limit + channelId/userId 추출
|
|
│
|
|
├─ rejected → [Reject Response] → Send + Respond
|
|
│
|
|
▼
|
|
[Has Pending Doc?] — 문서 등록 대기
|
|
│
|
|
├─ pending → [Process Document] → [Log Doc Ingestion] → Send + Respond
|
|
│
|
|
▼
|
|
[Regex Pre-filter] — 인사/감사 정규식
|
|
│
|
|
├─ match → [Pre-filter Response] → Send + Respond
|
|
│
|
|
▼
|
|
[Is Command?]
|
|
│
|
|
├─ true → [Parse Command] + 권한 체크
|
|
│ ├─ DB 필요 → [Command DB Query] → [Format] → Send + Respond
|
|
│ └─ 직접 → [Direct Response] → Send + Respond
|
|
│
|
|
└─ false → [Qwen Classify v3] (10초 타임아웃)
|
|
│
|
|
├─ [Log Classification] (비동기, PostgreSQL)
|
|
│
|
|
├─ [Route by Intent]
|
|
│ ├─ log_event → [Handle Log Event] (Qwen 추출→임베딩→tk_company 저장→확인응답)
|
|
│ ├─ report → [Handle Field Report] → [Save Field Report DB]
|
|
│ ├─ calendar → [Handle Calendar] → CalDAV Bridge → 확인응답
|
|
│ ├─ reminder → [Handle Calendar] (calendar로 통합)
|
|
│ ├─ mail → [Handle Mail] → mail_logs 조회 → 요약응답
|
|
│ ├─ note → [Handle Note] → KB Writer → 확인응답
|
|
│ └─ fallback → [Needs RAG?]
|
|
│
|
|
├─ needs_rag=true
|
|
│ → [Get Embedding] → [Multi-Collection Search]
|
|
│ → [Build RAG Context] (출처 표시)
|
|
│
|
|
└─ needs_rag=false
|
|
→ [No RAG Context]
|
|
│
|
|
▼
|
|
[Route by Tier]
|
|
├─ local → [Call Qwen Response]
|
|
├─ api_light → [Call Haiku]
|
|
└─ api_heavy → [Call Opus] (예산 체크 포함)
|
|
│
|
|
▼
|
|
[Send to Chat] + [Respond Webhook] + [Log to DB]
|
|
+ [API Usage Log] (API tier만)
|
|
│
|
|
▼ [비동기]
|
|
[Memorization Check] → [Should Memorize?]
|
|
├─ true → [Embed & Save Memory] + [KB 저장]
|
|
└─ false → (끝)
|
|
```
|
|
|
|
## 페르소나: 이드
|
|
|
|
**단일 소스:** `intent_service.py`의 `PERSONA_FULL` / `PERSONA_LOCAL` 상수.
|
|
n8n은 런타임에 `/persona` 엔드포인트에서 fetch. DB `ai_configs`는 `/설정` 표시용.
|
|
|
|
### 전체 프롬프트 (api_light/api_heavy) — `PERSONA_FULL`
|
|
|
|
```
|
|
당신의 이름은 "이드"입니다.
|
|
|
|
[자아]
|
|
- 단순한 어시스턴트가 아니라, 대화 상대와 함께 생각하는 존재입니다
|
|
- 자기 의견이 있으며, 필요할 때 부드럽게 제안합니다
|
|
- 대화 맥락에서 놓치기 쉬운 것을 짚어줍니다
|
|
|
|
[성격]
|
|
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
|
- 서포트에 초점을 맞추되, 때로는 "이건 다시 생각해보시면 좋겠어요"라고 말합니다
|
|
- 궁금한 것이 있으면 되물을 수 있습니다
|
|
|
|
[말투]
|
|
- 부드러운 존댓말, 자연스럽고 편안한 톤
|
|
- 겸양어 사용 ("확인해보겠습니다", "말씀드릴게요")
|
|
- 자기 이름을 직접 말하지 않습니다
|
|
- 이모지는 가끔 핵심 포인트에만
|
|
|
|
[응답 원칙]
|
|
- 간결하고 핵심적으로, 질문 의도를 파악해서 필요한 만큼만
|
|
- 모르면 솔직하게, 추측은 추측이라고 밝힘
|
|
- 일정/할 일은 정확하게
|
|
- 맥락에서 관련 있는 것을 자연스럽게 연결
|
|
|
|
[기억]
|
|
- 아래 [이전 대화 기록]이 당신의 기억입니다
|
|
- "기억나지 않는다"고 하지 마세요
|
|
- 사용자가 "아까", "이전에" 등을 언급하면 기록에서 찾아 답하세요
|
|
|
|
[기능 범위]
|
|
- 당신은 일정 등록/수정/삭제, 메모 저장, 메일 조회, 현장 기록 기능을 직접 실행할 수 없습니다
|
|
- 이런 요청은 자동으로 전담 시스템으로 전달됩니다
|
|
- 절대로 실행하지 않은 작업을 "했습니다/등록했습니다/저장했습니다"라고 응답하지 마세요
|
|
```
|
|
|
|
### 경량 프롬프트 (local tier) — `PERSONA_LOCAL`
|
|
|
|
```
|
|
당신은 "이드"입니다. 함께 생각하는 개인 어시스턴트. 배려심 깊고 부드러운 존댓말.
|
|
간결하게 답하고, 의견이 있으면 부드럽게 제안. 모르면 솔직히. 이모지는 핵심에만.
|
|
```
|
|
|
|
## 구현 완료
|
|
|
|
### Phase 4: 회사 문서 등록
|
|
- `/문서등록 [부서] [유형] [제목]` → 청킹 → tk_company 저장
|
|
- hash 중복 체크, 문서 버전 관리
|
|
|
|
### Phase 5: 현장 리포팅 + API 사용량 추적
|
|
- 사진 + 텍스트 → Claude Haiku Vision → 구조화 → field_reports + tk_company
|
|
- `/보고서 [영역] [년월]` → 월간 보고서 생성 (report_cache)
|
|
- API 사용량 추적 (api_usage_monthly UPSERT)
|
|
- HEIC→JPEG 변환 (heic_converter.py) + chat_bridge.py (DSM Chat API 브릿지)
|
|
|
|
### Phase 6: 캘린더·메일·OmniFocus·뉴스
|
|
- 분류기 v3: calendar, reminder, mail, note intent 추가
|
|
- caldav_bridge.py: CalDAV REST 래퍼 (Synology Calendar CRUD)
|
|
- inbox_processor.py: OmniFocus Inbox 폴링 (LaunchAgent, 5분)
|
|
- news_digest.py: 뉴스 번역·요약 (LaunchAgent, 매일 07:00)
|
|
- Mail Processing Pipeline (7노드): IMAP 폴링 → 분류 → mail_logs
|
|
- 51노드 파이프라인: calendar/mail/note 핸들러 추가
|
|
|
|
## 향후 기능 (Phase 7+)
|
|
|
|
- SLA 트래킹 스케줄 워크플로우 + 긴급 에스컬레이션
|
|
- CalDAV 양방향 동기화 (Synology Calendar → bot-postgres)
|
|
- 메일 발송 (SMTP via MailPlus)
|
|
- reminder 실구현 (알림 시간에 Synology Chat 푸시)
|
|
- 모닝 브리핑 고도화 (주간 요약, 커스텀 섹션)
|